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 +33 -0
- package/README.md +12 -9
- package/docs/cursor-live-smoke-checklist.md +4 -4
- package/docs/cursor-model-ux-spec.md +19 -16
- 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/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/scenarios.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-state.ts +3 -4
- package/src/cursor-tool-manifest.ts +16 -13
- package/src/cursor-usage-accounting.ts +55 -2
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
|
|
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
|
|
243
|
-
cursor plan
|
|
244
|
-
cursor
|
|
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
|
-
|
|
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
|
|
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
|
|
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 `
|
|
405
|
+
### I do not see `cursor-fast:on` / `cursor-fast:off` or `plan` in the footer
|
|
403
406
|
|
|
404
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -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()`
|
|
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
|
|
381
|
+
Status examples:
|
|
382
382
|
|
|
383
383
|
```text
|
|
384
|
-
cursor
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
706
|
-
- verify Cursor
|
|
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
|
|
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
|
@@ -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\.
|
|
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\\.
|
|
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
|
|
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);
|
package/src/cursor-state.ts
CHANGED
|
@@ -203,16 +203,15 @@ export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
|
|
|
203
203
|
return resolution.mode;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function formatCursorStatus(fast: boolean | undefined): string
|
|
207
|
-
const parts
|
|
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.
|
|
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
|
|
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
|
}
|