pi-cursor-sdk 0.1.52 → 0.1.54

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 CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.54 - 2026-07-02
6
+
7
+ ### Changed
8
+
9
+ - Show Cursor fast state explicitly in the interactive footer for fast-capable Cursor models as `cursor-fast:on` or `cursor-fast:off`, while preserving plain `cursor` status for Cursor models without fast support and clearing status for non-Cursor models.
10
+
11
+ ### Fixed
12
+
13
+ - Accept wrapped `package.json` read-card paths in platform-smoke visual assertions while keeping JSONL result checks as the source of truth for the actual bridge read target.
14
+
15
+ ### Validation
16
+
17
+ - `npm test`, `npm run typecheck`, `npm run typecheck:tests`, `npm pack --dry-run`, `npm run smoke:platform:all`, and subagent review loop pass locally.
18
+
19
+ ## 0.1.53 - 2026-06-29
20
+
21
+ ### Changed
22
+
23
+ - 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.
24
+ - 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.
25
+
26
+ ### Fixed
27
+
28
+ - 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`.
29
+
30
+ ### Validation
31
+
32
+ - 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.
33
+ - 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.
34
+ - `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.
35
+ - Focused live smokes verified exact-output, Cursor-native file read, pi bridge `pi__read`, and corrected footer context percentages after split tool turns.
36
+ - `npm run verify`, `npm pack --dry-run`, and `npm run smoke:platform:all` pass locally; release-review subagent returned no findings.
37
+
5
38
  ## 0.1.52 - 2026-06-28
6
39
 
7
40
  ### Fixed
package/README.md CHANGED
@@ -236,15 +236,18 @@ The `:fast` and `:slow` aliases are available only for Cursor models whose catal
236
236
 
237
237
  Composer 2 and Composer 2.5 can default to fast. Use `--cursor-no-fast` or a `:slow` virtual alias for a one-shot no-fast Composer run. In print mode (`-p`), `--cursor-no-fast` is silent and does not write `~/.pi/agent/cursor-sdk.json`.
238
238
 
239
- In interactive mode, the footer only shows fast mode when fast is enabled and Cursor mode when it is non-default. Fast and plan mode share one Cursor status value, so they do not overwrite each other:
239
+ In interactive mode, the footer shows Cursor status only while a Cursor model is active. Fast-capable models show fast state explicitly, and fast and plan mode share one Cursor status value so they do not overwrite each other:
240
240
 
241
241
  ```text
242
- cursor fast
243
- cursor plan
244
- cursor fast · plan
242
+ cursor
243
+ cursor · plan
244
+ cursor-fast:off
245
+ cursor-fast:on
246
+ cursor-fast:off · plan
247
+ cursor-fast:on · plan
245
248
  ```
246
249
 
247
- If you do not see `cursor fast`, fast mode is off. If you do not see `cursor plan`, Cursor SDK mode is the default `agent` mode.
250
+ `cursor-fast:off` means fast mode is off. A plain `cursor` status means the active Cursor model does not expose a fast parameter. If you do not see `plan`, Cursor SDK mode is the default `agent` mode.
248
251
 
249
252
  ## Cursor SDK mode
250
253
 
@@ -317,7 +320,7 @@ PI_CURSOR_TOOL_MANIFEST=0 pi --model cursor/composer-2-5
317
320
  PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1 pi --model cursor/composer-2-5
318
321
  ```
319
322
 
320
- On bootstrap sends, a compact **callable tool surfaces** block is injected into the Cursor prompt by default so models see host-tool categories, exposed `pi__*` bridge names for the current run, and a reminder that configured Cursor MCP servers are discovered at runtime (not via pi's tool catalog). It also states that pi's `--no-tools` disables pi tools/bridge exposure only; Cursor SDK host tools and configured Cursor MCP remain controlled by Cursor. Disable with `PI_CURSOR_TOOL_MANIFEST=0`.
323
+ 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
324
 
322
325
  `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
326
 
@@ -354,7 +357,7 @@ Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-k
354
357
  - **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
358
  - **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
359
  - **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
357
- - **Token usage is approximate in pi.** Cursor SDK usage events include cumulative internal agent/tool/cache work, so raw Cursor SDK counters are not copied into pi usage. The extension reports an additive replayable-context estimate instead: `output` estimates visible assistant output, `input` estimates the replayable Cursor context before that output, cache fields are zero, and `totalTokens = input + output + cacheRead + cacheWrite`. This keeps pi context display and compaction sizing consistent while avoiding Cursor's cumulative internal counters.
360
+ - **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
361
 
359
362
  ## Troubleshooting
360
363
 
@@ -399,9 +402,9 @@ pi install npm:pi-cursor-sdk
399
402
 
400
403
  That does not mean the model cannot think. It means the Cursor SDK does not expose a pi-controllable thinking parameter for that model. The model may still think internally and may still emit thinking deltas that pi renders natively.
401
404
 
402
- ### I do not see `cursor fast` or `cursor plan` in the footer
405
+ ### I do not see `cursor-fast:on` / `cursor-fast:off` or `plan` in the footer
403
406
 
404
- Fast mode is currently off when `cursor fast` is absent. Cursor SDK mode is the default `agent` mode when `cursor plan` is absent. When both are active, pi shows one combined Cursor status: `cursor fast · plan`.
407
+ The Cursor footer appears only while a Cursor model is active. Fast-capable models show `cursor-fast:on` or `cursor-fast:off`; Cursor models without a fast parameter show plain `cursor`. Cursor SDK mode is the default `agent` mode when `plan` is absent. When both are active, pi shows one combined Cursor status such as `cursor-fast:on · plan` or `cursor · plan`.
405
408
 
406
409
  ### My Cursor app settings or rules do not seem to apply
407
410
 
@@ -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` equal to `0`.
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
 
@@ -124,7 +124,7 @@ Observe with `tmux capture-pane -pt "$SESSION"` or attach manually.
124
124
 
125
125
  Pass criteria:
126
126
 
127
- - Footer shows `(cursor) composer-2-5`. With `--cursor-no-fast`, Cursor fast mode is off and the Cursor extension status should not show `cursor fast`; ignore unrelated status text from other extensions.
127
+ - Footer shows `(cursor) composer-2-5`. With `--cursor-no-fast`, Cursor fast mode is off and the Cursor extension status should show `cursor-fast:off`; ignore unrelated status text from other extensions.
128
128
  - The run uses pi 0.80.2 `--session-id` successfully.
129
129
  - Assistant answer appears correctly.
130
130
  - `/session` shows one user and one assistant message for the simple run.
@@ -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 or nonzero cache fields.
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 exactly `0`.
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 report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension does not copy raw Cursor SDK usage into pi usage or compaction. For Cursor assistant messages, `usage.output` estimates visible assistant output, `usage.input` estimates the replayable Cursor context before that output, cache fields are zero, and `usage.totalTokens = input + output + cacheRead + cacheWrite`. The input/total estimate is derived from the same `buildCursorPrompt()` path used for `Agent.send` so context display and compaction keep using replayable Cursor context sizing while pi session summaries remain additive. `src/cursor-usage-accounting.ts` owns this usage policy.
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.
@@ -370,7 +370,7 @@ Rules:
370
370
  - Toggle unsuffixed models with `/cursor-fast`; do not persist a new default while a virtual fast alias is selected.
371
371
  - Store per-session and global per-base-model preferences for unsuffixed models.
372
372
  - When calling `Agent.create()` or `agent.send()`, include the selected `fast` value in Cursor model params.
373
- - Show `fast` through `ctx.ui.setStatus()` when enabled.
373
+ - Show fast-capable models as `cursor-fast:on` or `cursor-fast:off` through `ctx.ui.setStatus()` while a Cursor model is active.
374
374
  - Keep `--cursor-fast` and `--cursor-no-fast` as explicit process-level force flags.
375
375
 
376
376
  Reason:
@@ -378,10 +378,11 @@ Reason:
378
378
  - `fast` does not affect pi `contextWindow`, thinking levels, or input support.
379
379
  - The virtual aliases trade small `--list-models` noise for per-agent selection that works with subagents and dynamic workflows, where mutating a shared global fast default is the wrong abstraction.
380
380
 
381
- Status example:
381
+ Status examples:
382
382
 
383
383
  ```text
384
- cursor fast
384
+ cursor-fast:off
385
+ cursor-fast:on
385
386
  ```
386
387
 
387
388
  ## Cursor SDK Mode Behavior
@@ -409,8 +410,9 @@ Rules:
409
410
  Status examples:
410
411
 
411
412
  ```text
412
- cursor plan
413
- cursor fast · plan
413
+ cursor · plan
414
+ cursor-fast:off · plan
415
+ cursor-fast:on · plan
414
416
  ```
415
417
 
416
418
  ## Footer Behavior
@@ -419,7 +421,7 @@ Hard requirement:
419
421
 
420
422
  - Leave pi's default footer intact.
421
423
  - Do not use `ctx.ui.setFooter()` for the first pass.
422
- - Use `ctx.ui.setStatus()` only for Cursor-only state that pi cannot show natively, such as `fast` and non-default Cursor SDK `plan` mode.
424
+ - Use `ctx.ui.setStatus()` only while a Cursor model is active, showing Cursor-only state that pi cannot show natively, such as `cursor-fast:on`, `cursor-fast:off`, and non-default Cursor SDK `plan` mode.
423
425
  - Non-cursor models must have no Cursor status.
424
426
 
425
427
  Reason:
@@ -433,13 +435,13 @@ Expected native footer behavior:
433
435
  - provider/model is shown by pi from the selected `cursor` model,
434
436
  - thinking level is shown by pi when `reasoning` is true,
435
437
  - context usage is computed from `contextWindow`,
436
- - extension status adds only Cursor-only text such as `cursor fast`, `cursor plan`, or `cursor fast · plan`.
438
+ - extension status adds only Cursor-only text such as `cursor`, `cursor · plan`, `cursor-fast:off`, `cursor-fast:on`, or `cursor-fast:on · plan`.
437
439
 
438
440
  `ctx.ui.setStatus()` adds an extension status line in the default footer. It does not patch the built-in model segment. The native shape is closer to:
439
441
 
440
442
  ```text
441
443
  ... (cursor) gpt-5.5@1m • medium
442
- cursor fast · plan
444
+ cursor-fast:off · plan
443
445
  ```
444
446
 
445
447
  not:
@@ -569,14 +571,14 @@ Initial Cursor default for Composer 2.5:
569
571
  pi model: cursor/composer-2-5
570
572
  Cursor params: fast=true
571
573
  pi thinking: off
572
- Cursor status: cursor fast
574
+ Cursor status: cursor-fast:on
573
575
  ```
574
576
 
575
577
  Toggle fast:
576
578
 
577
579
  ```text
578
580
  Cursor params: fast=false
579
- Cursor status: cleared
581
+ Cursor status: cursor-fast:off
580
582
  ```
581
583
 
582
584
  `shift+tab`: no-op because the model is not reasoning-capable.
@@ -589,7 +591,7 @@ Initial Cursor default:
589
591
  pi model: cursor/gpt-5.5@1m
590
592
  Cursor params: context=1m; reasoning=medium; fast=false
591
593
  pi thinking: medium
592
- Cursor status: cleared
594
+ Cursor status: cursor-fast:off
593
595
  ```
594
596
 
595
597
  After selecting the 272k variant:
@@ -604,7 +606,7 @@ After fast toggle:
604
606
 
605
607
  ```text
606
608
  Cursor params: context=272k; reasoning=medium; fast=true
607
- Cursor status: cursor fast
609
+ Cursor status: cursor-fast:on
608
610
  ```
609
611
 
610
612
  After `shift+tab` to xhigh:
@@ -622,7 +624,7 @@ Initial Cursor default:
622
624
  pi model: cursor/gpt-5.3-codex
623
625
  Cursor params: reasoning=high; fast=true
624
626
  pi thinking: high
625
- Cursor status: cursor fast
627
+ Cursor status: cursor-fast:on
626
628
  ```
627
629
 
628
630
  After `shift+tab` to low:
@@ -702,8 +704,9 @@ Before calling done:
702
704
  - confirm context variants show expected `context` column
703
705
  - launch interactive with Cursor
704
706
  - verify default pi footer remains unchanged
705
- - verify Cursor `fast` status appears only when enabled
706
- - verify Cursor `plan` status appears only in non-default mode and combines with fast as `cursor fast · plan`
707
+ - verify Cursor status appears only for Cursor models
708
+ - verify Cursor fast-capable models show `cursor-fast:on` or `cursor-fast:off`
709
+ - verify Cursor `plan` status appears only in non-default mode and combines with status as `cursor · plan`, `cursor-fast:on · plan`, or `cursor-fast:off · plan`
707
710
  - verify non-cursor footer/status unchanged
708
711
  - verify `shift+tab` uses pi native thinking
709
712
  - verify context changes through native model selection
@@ -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 keeps Cursor SDK internal counters out of pi usage and reports an additive replayable-context estimate: `output` estimates visible assistant activity, `input` estimates the replayable Cursor context before that output, cache fields are zero, and `usage.totalTokens = input + output + cacheRead + cacheWrite`.
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 lists host-tool categories, bridge `pi__*` names for the current run, and a reminder that configured Cursor MCP servers appear at runtime via `listTools`. MCP `listTools` entries for bridged pi tools point back to the bootstrap prompt instead of repeating the full contract.
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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@ const CARD_PATTERNS = [
18
18
  { id: "write", pattern: /^\s*\+.*beta\s*$/i },
19
19
  { id: "edit-diff", pattern: /^\s*\+.*gamma\s*$/i },
20
20
  { id: "shell-failure", pattern: /^\s*(?:native shell failure|Command exited with code 7)\s*$/i },
21
- { id: "bridge-read-success", pattern: /^\s*read (?:\.\/package\.json|.*[\\/]package\.json)\s*$/i },
21
+ { id: "bridge-read-success", pattern: /^\s*read (?:\.\/package\.json|.*[\\/]package\.j(?:son|s))\s*$/i },
22
22
  { id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
23
23
  { id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
24
24
  { id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
@@ -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 });
@@ -133,7 +133,7 @@ BRIDGE_MATRIX_OK bash_ok=<yes/no> read_ok=<yes/no> read_missing_error=<yes/no>`,
133
133
  ],
134
134
  expectedJSONLResultToolOrder: ["bash", "read", "read"],
135
135
  visualEvidence: [
136
- { id: "bridge-read-success", pattern: "^\\s*read (?:\\./package\\.json|.*[\\\\/]package\\.json)", jsonlResultId: "bridge-read-success" },
136
+ { id: "bridge-read-success", pattern: "^\\s*read (?:\\./package\\.json|.*[\\\\/]package\\.j(?:son|s))", jsonlResultId: "bridge-read-success" },
137
137
  { id: "bridge-read-failure", pattern: "^\\s*read \\./definitely-missing-platform-smoke-file\\.txt|ENOENT: no such file", jsonlResultId: "bridge-read-failure" },
138
138
  { id: "bridge-shell-success", pattern: "^\\s*bridge visual smoke\\s*$", jsonlResultId: "bridge-shell-success" },
139
139
  ],
@@ -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 !== 0) cacheZero = false;
813
- if (typeof usage.cacheWrite === "number" && usage.cacheWrite !== 0) cacheZero = false;
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, cacheZero };
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 !== 0 ||
83
- usage.cacheWrite !== 0
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(agentMode: AgentModeOption | undefined): string | undefined {
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
- "Exposed pi__* bridge tools are also callable in plan mode when the user asks for them or they are needed to answer.",
31
- ].join("\n");
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 an explicit `cd` to the repo path when running project commands; session cwd may not match paths in tool args.",
39
- options.includePlanModeGuidance === false ? undefined : getCursorPlanModeToolGuidanceText(options.agentMode),
40
- "Exact-output requests: if the latest user asks to reply exactly, output exactly that text and do not add preambles, diagnostics, or repo checks unless explicitly requested.",
41
- "Tool boundary reminder: If a tool is needed, call an available Cursor SDK/MCP tool. Never print a tool card (for example Tool call/Shell/command) as assistant text.",
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(options: Pick<CursorPromptOptions, "agentMode"> & { hasToolManifest?: boolean } = {}): string {
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 by Cursor SDK in this run. Pi tool names, replay labels, and transcript names are context only—not callable.",
49
- "Bridged pi tools: call pi__* MCP names when exposed, not the pi card name in history. Replay activity is display-only.",
50
- "Do not claim pi-side or WebSearch/WebFetch tools unless Cursor executes an equivalent tool.",
51
- "Use pi__cursor_ask_question for material choices if exposed.",
52
- getCursorPlanModeToolGuidanceText(options.agentMode),
53
- "Images: only the latest user message's images are sent as bytes; ask to reattach or describe prior images.",
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 tool surfaces block below.");
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({ agentMode: options.agentMode, hasToolManifest: Boolean(options.toolManifest) })];
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 { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
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
- applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
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
- applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
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
- applyCursorApproximateUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens));
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 { applyCursorApproximateUsage } from "./cursor-usage-accounting.js";
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
- applyCursorApproximateUsage(partial, model, context, prepared.meta.promptInputTokens);
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 = { ...getCursorPromptOptions(model), agentMode };
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);
@@ -203,16 +203,15 @@ export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
203
203
  return resolution.mode;
204
204
  }
205
205
 
206
- function formatCursorStatus(fast: boolean | undefined): string | undefined {
207
- const parts: string[] = [];
206
+ function formatCursorStatus(fast: boolean | undefined): string {
207
+ const parts = [fast === true ? "cursor-fast:on" : fast === false ? "cursor-fast:off" : "cursor"];
208
208
  const modeResolution = resolveCursorAgentMode();
209
- if (fast === true) parts.push("fast");
210
209
  if (modeResolution.kind === "invalid") {
211
210
  parts.push("mode invalid");
212
211
  } else if (modeResolution.mode === "plan") {
213
212
  parts.push("plan");
214
213
  }
215
- return parts.length > 0 ? `cursor ${parts.join(" · ")}` : undefined;
214
+ return parts.join(" · ");
216
215
  }
217
216
 
218
217
  function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
@@ -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, shell, grep, glob, ls, edit, write, delete, readLints, updateTodos, createPlan, task, generateImage, mcp, semSearch, recordScreen, and web search/fetch when exposed";
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 SDK host tools (callable; not listed in MCP listTools): ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}.`,
28
- "- Configured Cursor MCP servers: discovered at runtime via MCP listTools (depends on Cursor settings and PI_CURSOR_SETTING_SOURCES).",
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 (!piBridgeEnabled) {
33
- lines.push("- Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
34
- } else if (bridgeTools.length === 0) {
35
- lines.push("- Pi bridge: no pi__* tools exposed this run.");
36
- } else {
37
- const names = [...bridgeTools.map((tool) => tool.mcpToolName)].sort().join(", ");
38
- lines.push(`- Pi bridge (call pi__* MCP names; pi shows real pi tool names): ${names}.`);
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 tool names, and transcript labels.");
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 applyCursorApproximateUsage(partial: AssistantMessage, _model: Model<Api>, _context: Context, sessionInputTokens: number): void {
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 = partial.usage.input + partial.usage.output + partial.usage.cacheRead + partial.usage.cacheWrite;
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
  }