cursor-opencode-provider 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +230 -44
  3. package/SECURITY.md +60 -12
  4. package/dist/activity.d.ts +73 -1
  5. package/dist/activity.js +231 -5
  6. package/dist/agent-url.js +37 -9
  7. package/dist/auth-renewal.d.ts +1 -1
  8. package/dist/auth-renewal.js +58 -26
  9. package/dist/billing.d.ts +105 -0
  10. package/dist/billing.js +212 -0
  11. package/dist/classic-tool-results.d.ts +1 -0
  12. package/dist/classic-tool-results.js +42 -0
  13. package/dist/context/build.d.ts +23 -4
  14. package/dist/context/build.js +65 -10
  15. package/dist/context/dynamic-catalog.js +6 -1
  16. package/dist/context/env.js +27 -3
  17. package/dist/context/epoch.js +9 -5
  18. package/dist/context/frozen.js +6 -2
  19. package/dist/context/git.d.ts +5 -2
  20. package/dist/context/git.js +17 -7
  21. package/dist/context/index.d.ts +2 -0
  22. package/dist/context/index.js +2 -0
  23. package/dist/context/paths.d.ts +45 -18
  24. package/dist/context/paths.js +72 -22
  25. package/dist/context/skills-bridge.d.ts +22 -0
  26. package/dist/context/skills-bridge.js +20 -0
  27. package/dist/context/skills.d.ts +34 -0
  28. package/dist/context/skills.js +211 -0
  29. package/dist/debug.js +13 -2
  30. package/dist/early-tool-rows.d.ts +60 -0
  31. package/dist/early-tool-rows.js +96 -0
  32. package/dist/errors.d.ts +49 -7
  33. package/dist/errors.js +163 -8
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +117 -13
  36. package/dist/host-interrupt.d.ts +12 -0
  37. package/dist/host-interrupt.js +18 -0
  38. package/dist/host-plan-file.d.ts +28 -0
  39. package/dist/host-plan-file.js +76 -0
  40. package/dist/host-steer.d.ts +71 -0
  41. package/dist/host-steer.js +232 -0
  42. package/dist/image-input.d.ts +18 -0
  43. package/dist/image-input.js +122 -30
  44. package/dist/image-save.d.ts +10 -2
  45. package/dist/image-save.js +33 -21
  46. package/dist/index.d.ts +2 -2
  47. package/dist/language-model.d.ts +149 -30
  48. package/dist/language-model.js +3127 -673
  49. package/dist/lost-request.d.ts +39 -0
  50. package/dist/lost-request.js +111 -0
  51. package/dist/model-config.d.ts +12 -1
  52. package/dist/model-config.js +78 -30
  53. package/dist/models.d.ts +26 -3
  54. package/dist/models.js +101 -45
  55. package/dist/opencode2/catalog.d.ts +1 -0
  56. package/dist/opencode2/catalog.js +2 -0
  57. package/dist/opencode2/image-save-tool.d.ts +8 -0
  58. package/dist/opencode2/image-save-tool.js +84 -0
  59. package/dist/opencode2/tool-results.d.ts +18 -0
  60. package/dist/opencode2/tool-results.js +66 -0
  61. package/dist/opencode2/types.d.ts +15 -0
  62. package/dist/parallel-step.d.ts +65 -0
  63. package/dist/parallel-step.js +102 -0
  64. package/dist/plugin-opencode2.js +233 -65
  65. package/dist/plugin.js +97 -23
  66. package/dist/pricing-data.d.ts +40 -3
  67. package/dist/pricing-data.js +40 -3
  68. package/dist/pricing.d.ts +1 -1
  69. package/dist/pricing.js +7 -4
  70. package/dist/process-shared.d.ts +14 -0
  71. package/dist/process-shared.js +18 -0
  72. package/dist/protocol/apply-patch.d.ts +25 -13
  73. package/dist/protocol/apply-patch.js +79 -30
  74. package/dist/protocol/ask-question.d.ts +12 -1
  75. package/dist/protocol/ask-question.js +77 -3
  76. package/dist/protocol/blob-store.d.ts +5 -1
  77. package/dist/protocol/blob-store.js +10 -6
  78. package/dist/protocol/client-version.js +6 -4
  79. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  80. package/dist/protocol/conversation-cache-header.js +149 -0
  81. package/dist/protocol/conversation-persistence.d.ts +27 -1
  82. package/dist/protocol/conversation-persistence.js +628 -126
  83. package/dist/protocol/conversation-state.d.ts +8 -1
  84. package/dist/protocol/conversation-state.js +28 -7
  85. package/dist/protocol/create-plan.d.ts +47 -25
  86. package/dist/protocol/create-plan.js +33 -23
  87. package/dist/protocol/host-notes.d.ts +31 -0
  88. package/dist/protocol/host-notes.js +85 -0
  89. package/dist/protocol/interactions.d.ts +26 -17
  90. package/dist/protocol/interactions.js +80 -31
  91. package/dist/protocol/messages.js +164 -21
  92. package/dist/protocol/request.d.ts +40 -19
  93. package/dist/protocol/request.js +55 -28
  94. package/dist/protocol/sent-history-images.d.ts +49 -0
  95. package/dist/protocol/sent-history-images.js +102 -0
  96. package/dist/protocol/switch-mode.d.ts +66 -27
  97. package/dist/protocol/switch-mode.js +147 -73
  98. package/dist/protocol/token-details.d.ts +2 -0
  99. package/dist/protocol/token-details.js +11 -1
  100. package/dist/protocol/tool-call-bridge.d.ts +6 -0
  101. package/dist/protocol/tool-call-bridge.js +24 -0
  102. package/dist/protocol/tools.d.ts +90 -9
  103. package/dist/protocol/tools.js +543 -151
  104. package/dist/protocol/turn-provenance.d.ts +19 -4
  105. package/dist/protocol/turn-provenance.js +77 -20
  106. package/dist/replay-safety.d.ts +22 -4
  107. package/dist/replay-safety.js +86 -27
  108. package/dist/retry-budget.d.ts +21 -0
  109. package/dist/retry-budget.js +94 -0
  110. package/dist/runtime-files.d.ts +23 -0
  111. package/dist/runtime-files.js +581 -0
  112. package/dist/session.d.ts +129 -10
  113. package/dist/session.js +198 -35
  114. package/dist/shared.d.ts +1 -1
  115. package/dist/shared.js +1 -1
  116. package/dist/shell-timeout.d.ts +54 -10
  117. package/dist/shell-timeout.js +362 -92
  118. package/dist/stream-write.d.ts +20 -0
  119. package/dist/stream-write.js +87 -0
  120. package/dist/terminal-runner.d.ts +15 -0
  121. package/dist/terminal-runner.js +528 -0
  122. package/dist/todo-store.d.ts +2 -0
  123. package/dist/todo-store.js +19 -0
  124. package/dist/transport/connect.d.ts +10 -1
  125. package/dist/transport/connect.js +124 -27
  126. package/dist/usage.d.ts +37 -17
  127. package/dist/usage.js +85 -34
  128. package/package.json +5 -3
  129. package/dist/plan-execution-kickoff.d.ts +0 -57
  130. package/dist/plan-execution-kickoff.js +0 -186
  131. package/dist/protocol/thinking.d.ts +0 -15
  132. package/dist/protocol/thinking.js +0 -17
@@ -12,16 +12,17 @@ export type RunRequestInput = {
12
12
  /** Stable parent group; unlike conversationId, this survives compaction/rebase. */
13
13
  conversationGroupId?: string;
14
14
  /**
15
- * Prior chat turns for a seed ConversationStateStructure (no checkpoint).
16
- * Tool outputs, when required for compaction/recovery, are represented as
17
- * user-role OpenCode host observations rather than assistant-authored prose.
15
+ * Prior chat turns for a Run without a checkpoint, carried in the live user
16
+ * text (see {@link seedHistoryUserText}). Tool outputs, when required for
17
+ * compaction/recovery, are represented as user-role OpenCode host
18
+ * observations rather than assistant-authored prose.
18
19
  */
19
20
  history?: SeedHistoryMessage[];
20
21
  /**
21
22
  * Opaque ConversationStateStructure bytes from the last
22
23
  * conversation_checkpoint_update for this conversation_id. When set, echoed
23
- * as AgentRunRequest.conversation_state (CLI parity). When absent, a seed
24
- * state carrying only `history` (or empty) is built for turn 1.
24
+ * as AgentRunRequest.conversation_state (CLI parity). When absent, an empty
25
+ * seed state is sent and `history` travels in the user text.
25
26
  */
26
27
  conversationState?: Uint8Array;
27
28
  parameterValues?: Array<{
@@ -37,28 +38,39 @@ export type RunRequestInput = {
37
38
  requestContext?: Record<string, unknown>;
38
39
  /** Resume the supplied checkpoint instead of submitting another user turn. */
39
40
  action?: "user" | "resume";
41
+ /**
42
+ * Cursor mode of this user turn (`UserMessage.mode`, an `agent.v1.AgentMode`
43
+ * value). Cursor CLI sends its current mode on every user message.
44
+ */
45
+ mode?: number;
40
46
  };
41
47
  /**
42
- * Seed ConversationStateStructure for the first turn (no checkpoint yet).
48
+ * Empty ConversationStateStructure for a Run without a checkpoint (turn 1,
49
+ * compaction, rebase, reseed). After the first checkpoint arrives we stop
50
+ * inventing state and echo the server's opaque structure instead (CLI behavior).
43
51
  *
44
- * After the first checkpoint arrives we stop inventing state and echo the
45
- * server's opaque structure instead (CLI behavior). Compaction resets and
46
- * rebases also use this seed, with `history` carrying OpenCode's prompt turns
47
- * so Cursor can continue without the old checkpoint.
48
- *
49
- * No `system` entry is seeded: Cursor does not follow a client-authored
50
- * `system` root message, and neither Cursor client writes
51
- * `root_prompt_messages_json`. Host system context travels as the
52
- * system-instructions rule in RequestContext (`systemInstructionsRule`), so
53
- * `system` history entries are dropped here rather than duplicated.
52
+ * Nothing goes in `root_prompt_messages_json` (#1). Neither Cursor client
53
+ * writes it: Cursor checkpoints fill it with references to the conversation's
54
+ * own rendered root prompt. A client-written entry stands in for that prompt,
55
+ * so Cursor stops rendering RequestContext rules, skills and subagents — the
56
+ * host system-instructions rule included. Verified live 2026-10-09
57
+ * (claude-sonnet-5-5): two seeded history messages there gave checkpoint
58
+ * categories `rules:0, subagents:0` and a reply that ignored the rule; the same
59
+ * history in the user text kept both. History therefore travels in the user
60
+ * text ({@link seedHistoryUserText}).
54
61
  *
55
62
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
56
63
  * field is the internal `--system-prompt` CLI override and the server rejects
57
64
  * it for normal accounts.
58
65
  */
59
- export declare function buildSeedConversationState(input?: {
60
- history?: SeedHistoryMessage[];
61
- }): Uint8Array;
66
+ export declare function buildSeedConversationState(): Uint8Array;
67
+ /**
68
+ * Prior turns of a Run without a checkpoint, as a transcript ahead of the live
69
+ * user text. `system` entries are dropped: host system context travels as the
70
+ * system-instructions rule in RequestContext. Closing tags inside a message are
71
+ * escaped so a message cannot end its own block early.
72
+ */
73
+ export declare function seedHistoryUserText(text: string, history: readonly SeedHistoryMessage[] | undefined): string;
62
74
  /**
63
75
  * Build an AgentClientMessage{run_request} for a conversation turn.
64
76
  *
@@ -70,3 +82,12 @@ export declare function buildRunRequest(input: RunRequestInput): Uint8Array;
70
82
  * Build a heartbeat message.
71
83
  */
72
84
  export declare function buildHeartbeat(): Uint8Array;
85
+ /**
86
+ * ACM #5 `exec_client_control_message.heartbeat{id}`: Cursor CLI sends it every
87
+ * 3 s for each exec it is still running, keyed by the exec message id. The CLI
88
+ * leaves a zero id out, as proto3 does for a default scalar; protobufjs would
89
+ * write it explicitly, so it is omitted here to send the CLI's exact bytes.
90
+ */
91
+ export declare function buildExecHeartbeat(execId: number): Uint8Array;
92
+ /** Stops the live Run, as Cursor CLI does on a user Stop before it closes the stream. */
93
+ export declare function buildCancelAction(reason: string): Uint8Array;
@@ -1,35 +1,42 @@
1
1
  import { encodeMessage, getMessageTypes } from "./messages.js";
2
2
  /**
3
- * Seed ConversationStateStructure for the first turn (no checkpoint yet).
3
+ * Empty ConversationStateStructure for a Run without a checkpoint (turn 1,
4
+ * compaction, rebase, reseed). After the first checkpoint arrives we stop
5
+ * inventing state and echo the server's opaque structure instead (CLI behavior).
4
6
  *
5
- * After the first checkpoint arrives we stop inventing state and echo the
6
- * server's opaque structure instead (CLI behavior). Compaction resets and
7
- * rebases also use this seed, with `history` carrying OpenCode's prompt turns
8
- * so Cursor can continue without the old checkpoint.
9
- *
10
- * No `system` entry is seeded: Cursor does not follow a client-authored
11
- * `system` root message, and neither Cursor client writes
12
- * `root_prompt_messages_json`. Host system context travels as the
13
- * system-instructions rule in RequestContext (`systemInstructionsRule`), so
14
- * `system` history entries are dropped here rather than duplicated.
7
+ * Nothing goes in `root_prompt_messages_json` (#1). Neither Cursor client
8
+ * writes it: Cursor checkpoints fill it with references to the conversation's
9
+ * own rendered root prompt. A client-written entry stands in for that prompt,
10
+ * so Cursor stops rendering RequestContext rules, skills and subagents — the
11
+ * host system-instructions rule included. Verified live 2026-10-09
12
+ * (claude-sonnet-5-5): two seeded history messages there gave checkpoint
13
+ * categories `rules:0, subagents:0` and a reply that ignored the rule; the same
14
+ * history in the user text kept both. History therefore travels in the user
15
+ * text ({@link seedHistoryUserText}).
15
16
  *
16
17
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
17
18
  * field is the internal `--system-prompt` CLI override and the server rejects
18
19
  * it for normal accounts.
19
20
  */
20
- export function buildSeedConversationState(input) {
21
- const root = getMessageTypes();
22
- const type = root.lookupType("ConversationStateStructure");
23
- const messages = [];
24
- for (const entry of input?.history ?? []) {
25
- if (!entry.content || entry.role === "system")
26
- continue;
27
- messages.push(JSON.stringify({ role: entry.role, content: entry.content }));
28
- }
29
- const obj = {};
30
- if (messages.length > 0)
31
- obj.root_prompt_messages_json = messages;
32
- return type.encode(type.fromObject(obj)).finish();
21
+ export function buildSeedConversationState() {
22
+ const type = getMessageTypes().lookupType("ConversationStateStructure");
23
+ return type.encode(type.fromObject({})).finish();
24
+ }
25
+ const SEED_HISTORY_TAGS = /<\/(conversation_history|user|assistant)>/gi;
26
+ /**
27
+ * Prior turns of a Run without a checkpoint, as a transcript ahead of the live
28
+ * user text. `system` entries are dropped: host system context travels as the
29
+ * system-instructions rule in RequestContext. Closing tags inside a message are
30
+ * escaped so a message cannot end its own block early.
31
+ */
32
+ export function seedHistoryUserText(text, history) {
33
+ const entries = (history ?? []).filter((entry) => entry.content && entry.role !== "system");
34
+ if (entries.length === 0)
35
+ return text;
36
+ const transcript = entries
37
+ .map((entry) => `<${entry.role}>\n${entry.content.replace(SEED_HISTORY_TAGS, "<\\/$1>")}\n</${entry.role}>`)
38
+ .join("\n\n");
39
+ return `<conversation_history>\n${transcript}\n</conversation_history>\n\n${text}`;
33
40
  }
34
41
  /**
35
42
  * Build an AgentClientMessage{run_request} for a conversation turn.
@@ -43,10 +50,13 @@ export function buildRunRequest(input) {
43
50
  // mcp_meta_tool_options. AgentRunRequest.mcp_tools (#4) stays empty on real
44
51
  // turns (CLI prewarm-only). Full defs are session.toolDescriptors + exec #36.
45
52
  const requestContext = input.requestContext;
53
+ const seeded = !(input.conversationState && input.conversationState.length > 0);
46
54
  const userMessage = {
47
- text: input.text,
55
+ text: seeded ? seedHistoryUserText(input.text, input.history) : input.text,
48
56
  message_id: msgId,
49
57
  };
58
+ if (input.mode !== undefined)
59
+ userMessage.mode = input.mode;
50
60
  if (input.images?.length) {
51
61
  userMessage.selected_context = {
52
62
  selected_images: input.images.map((image) => ({
@@ -63,9 +73,9 @@ export function buildRunRequest(input) {
63
73
  const action = input.action === "resume"
64
74
  ? { resume_action: {} }
65
75
  : { user_message_action: userMessageAction };
66
- const conversationState = input.conversationState && input.conversationState.length > 0
67
- ? input.conversationState
68
- : buildSeedConversationState({ history: input.history });
76
+ const conversationState = seeded
77
+ ? buildSeedConversationState()
78
+ : input.conversationState;
69
79
  const runRequest = {
70
80
  conversation_id: input.conversationId,
71
81
  conversation_group_id: input.conversationGroupId ?? input.conversationId,
@@ -96,3 +106,20 @@ export function buildHeartbeat() {
96
106
  client_heartbeat: {},
97
107
  });
98
108
  }
109
+ /**
110
+ * ACM #5 `exec_client_control_message.heartbeat{id}`: Cursor CLI sends it every
111
+ * 3 s for each exec it is still running, keyed by the exec message id. The CLI
112
+ * leaves a zero id out, as proto3 does for a default scalar; protobufjs would
113
+ * write it explicitly, so it is omitted here to send the CLI's exact bytes.
114
+ */
115
+ export function buildExecHeartbeat(execId) {
116
+ return encodeMessage("AgentClientMessage", {
117
+ exec_client_control_message: { heartbeat: execId === 0 ? {} : { id: execId } },
118
+ });
119
+ }
120
+ /** Stops the live Run, as Cursor CLI does on a user Stop before it closes the stream. */
121
+ export function buildCancelAction(reason) {
122
+ return encodeMessage("AgentClientMessage", {
123
+ conversation_action: { cancel_action: { reason } },
124
+ });
125
+ }
@@ -0,0 +1,49 @@
1
+ /** `hashes` is shared between entries and never mutated. */
2
+ type HeldImages = {
3
+ conversationId: string;
4
+ checkpoint: string;
5
+ hashes: readonly string[];
6
+ };
7
+ /**
8
+ * One Run's view: `held` by the checkpoint it last stood on, `checkpointed` sent
9
+ * before Cursor's latest checkpoint arrived, `sent` since, each with the last
10
+ * frame arrival at its send. Only a turn that ends on a checkpoint of its own
11
+ * moves `checkpointed` into `held`.
12
+ */
13
+ export type RunHistoryImages = {
14
+ held: readonly string[];
15
+ checkpointed: string[];
16
+ sent: Array<{
17
+ hash: string;
18
+ afterArrival: number;
19
+ }>;
20
+ };
21
+ export declare function checkpointFingerprint(checkpoint: Uint8Array): string;
22
+ /** The images `checkpoint` is known to hold. */
23
+ export declare function heldHistoryImageHashes(sessionKey: string, conversationId: string, checkpoint: Uint8Array): ReadonlySet<string> | undefined;
24
+ export declare function runHistoryImages(held?: ReadonlySet<string>): RunHistoryImages;
25
+ /** `lastArrival`: the last frame arrival once the images are written. */
26
+ export declare function noteHistoryImagesSent(run: RunHistoryImages, hashes: readonly string[], lastArrival?: number): void;
27
+ /**
28
+ * Cursor's checkpoints extend the one the Run started on, so each holds at least what that one held.
29
+ * A checkpoint that arrived before images were written (read only after a host tool's result) holds none of them.
30
+ */
31
+ export declare function noteHistoryImageCheckpoint(run: RunHistoryImages, sessionKey: string, conversationId: string, checkpoint: Uint8Array, arrival?: number): void;
32
+ /**
33
+ * The turn ended. On a checkpoint of its own, that checkpoint holds what the Run
34
+ * sent before it; without one, nothing the Run sent counts as held.
35
+ */
36
+ export declare function confirmHistoryImagesAtTurnEnd(run: RunHistoryImages, sessionKey: string, conversationId: string, checkpoint: Uint8Array | undefined): void;
37
+ /** For the restart snapshot: the set the checkpoint being saved holds. */
38
+ export declare function heldHistoryImagesSnapshot(sessionKey: string, conversationId: string, checkpoint: Uint8Array | undefined): {
39
+ checkpoint: string;
40
+ hashes: string[];
41
+ } | undefined;
42
+ export declare function restoreHeldHistoryImages(sessionKey: string, conversationId: string, saved: {
43
+ checkpoint?: string;
44
+ hashes?: readonly string[];
45
+ }): void;
46
+ export declare function forgetHeldHistoryImages(sessionKey: string): void;
47
+ export declare function snapshotHeldHistoryImagesForTests(sessionKey: string): HeldImages[];
48
+ export declare function resetHeldHistoryImagesForTests(): void;
49
+ export {};
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ // Content hashes of the history images a session's Cursor conversation holds, so
3
+ // growing history does not re-upload old screenshots. Each set belongs to one
4
+ // checkpoint: a Run skips an image only when it sends exactly that checkpoint.
5
+ /** Mirrors MAX_TURN_STATE_SESSIONS. */
6
+ const MAX_SESSIONS = 256;
7
+ const MAX_IMAGES_PER_SESSION = 256;
8
+ /** A stopped or failed turn leaves the next Run on one of its intermediate checkpoints. */
9
+ const MAX_CHECKPOINTS_PER_SESSION = 32;
10
+ /** Newest first. */
11
+ const heldBySession = new Map();
12
+ export function checkpointFingerprint(checkpoint) {
13
+ return createHash("sha256").update(checkpoint).digest("hex");
14
+ }
15
+ function newest(hashes) {
16
+ const unique = [...new Set([...hashes].reverse())].reverse();
17
+ return unique.slice(Math.max(0, unique.length - MAX_IMAGES_PER_SESSION));
18
+ }
19
+ function touch(sessionKey, entries) {
20
+ heldBySession.delete(sessionKey);
21
+ if (entries.length === 0)
22
+ return;
23
+ heldBySession.set(sessionKey, entries.slice(0, MAX_CHECKPOINTS_PER_SESSION));
24
+ while (heldBySession.size > MAX_SESSIONS) {
25
+ const oldest = heldBySession.keys().next().value;
26
+ if (!oldest)
27
+ break;
28
+ heldBySession.delete(oldest);
29
+ }
30
+ }
31
+ function find(sessionKey, conversationId, checkpoint) {
32
+ const entries = heldBySession.get(sessionKey);
33
+ if (!entries)
34
+ return undefined;
35
+ const fingerprint = checkpointFingerprint(checkpoint);
36
+ return entries.find((entry) => entry.conversationId === conversationId && entry.checkpoint === fingerprint);
37
+ }
38
+ /** The images `checkpoint` is known to hold. */
39
+ export function heldHistoryImageHashes(sessionKey, conversationId, checkpoint) {
40
+ const entry = find(sessionKey, conversationId, checkpoint);
41
+ if (!entry)
42
+ return undefined;
43
+ touch(sessionKey, heldBySession.get(sessionKey));
44
+ return new Set(entry.hashes);
45
+ }
46
+ function recordHeldHistoryImages(sessionKey, conversationId, checkpoint, hashes) {
47
+ if (hashes.length === 0)
48
+ return;
49
+ const entry = { conversationId, checkpoint: checkpointFingerprint(checkpoint), hashes };
50
+ const others = (heldBySession.get(sessionKey) ?? [])
51
+ .filter((other) => other.conversationId !== conversationId || other.checkpoint !== entry.checkpoint);
52
+ touch(sessionKey, [entry, ...others]);
53
+ }
54
+ export function runHistoryImages(held) {
55
+ return { held: [...(held ?? [])], checkpointed: [], sent: [] };
56
+ }
57
+ /** `lastArrival`: the last frame arrival once the images are written. */
58
+ export function noteHistoryImagesSent(run, hashes, lastArrival = 0) {
59
+ run.sent.push(...hashes.map((hash) => ({ hash, afterArrival: lastArrival })));
60
+ }
61
+ /**
62
+ * Cursor's checkpoints extend the one the Run started on, so each holds at least what that one held.
63
+ * A checkpoint that arrived before images were written (read only after a host tool's result) holds none of them.
64
+ */
65
+ export function noteHistoryImageCheckpoint(run, sessionKey, conversationId, checkpoint, arrival = Infinity) {
66
+ const later = run.sent.findIndex((image) => image.afterArrival >= arrival);
67
+ const earlier = later < 0 ? run.sent.length : later;
68
+ run.checkpointed.push(...run.sent.slice(0, earlier).map((image) => image.hash));
69
+ run.sent = run.sent.slice(earlier);
70
+ recordHeldHistoryImages(sessionKey, conversationId, checkpoint, run.held);
71
+ }
72
+ /**
73
+ * The turn ended. On a checkpoint of its own, that checkpoint holds what the Run
74
+ * sent before it; without one, nothing the Run sent counts as held.
75
+ */
76
+ export function confirmHistoryImagesAtTurnEnd(run, sessionKey, conversationId, checkpoint) {
77
+ if (checkpoint?.length) {
78
+ run.held = newest([...run.held, ...run.checkpointed]);
79
+ recordHeldHistoryImages(sessionKey, conversationId, checkpoint, run.held);
80
+ }
81
+ run.checkpointed = [];
82
+ run.sent = [];
83
+ }
84
+ /** For the restart snapshot: the set the checkpoint being saved holds. */
85
+ export function heldHistoryImagesSnapshot(sessionKey, conversationId, checkpoint) {
86
+ const entry = checkpoint?.length ? find(sessionKey, conversationId, checkpoint) : undefined;
87
+ return entry ? { checkpoint: entry.checkpoint, hashes: [...entry.hashes] } : undefined;
88
+ }
89
+ export function restoreHeldHistoryImages(sessionKey, conversationId, saved) {
90
+ touch(sessionKey, saved.checkpoint && saved.hashes?.length
91
+ ? [{ conversationId, checkpoint: saved.checkpoint, hashes: newest(saved.hashes) }]
92
+ : []);
93
+ }
94
+ export function forgetHeldHistoryImages(sessionKey) {
95
+ heldBySession.delete(sessionKey);
96
+ }
97
+ export function snapshotHeldHistoryImagesForTests(sessionKey) {
98
+ return (heldBySession.get(sessionKey) ?? []).map((entry) => ({ ...entry, hashes: [...entry.hashes] }));
99
+ }
100
+ export function resetHeldHistoryImagesForTests() {
101
+ heldBySession.clear();
102
+ }
@@ -17,18 +17,18 @@
17
17
  * through the Question service and, on "Yes", injects a synthetic user message
18
18
  * carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
19
19
  * `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
20
- * `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
21
- * observable halves — the approval gate and the behavioural contract — are both
22
- * reachable, so the protocol bridge degrades instead of refusing when the host
23
- * tool is missing. OpenCode 2 additionally exposes `session.switchAgent`; its
20
+ * `session/prompt.ts`). OpenCode 2 exposes `session.switchAgent`; its
24
21
  * entrypoint installs a structural callback that selects the native `plan` or
25
22
  * `build` primary agent after the owning Cursor Run becomes terminal:
26
23
  *
27
24
  * - entering plan/spec needs no host tool at all — approve immediately and let
28
25
  * the injected <system_reminder> carry the contract, exactly as Cursor CLI's
29
26
  * own plan mode is prompt-enforced.
30
- * - leaving plan mode is the gate the user must actually see, so it falls back
31
- * to the host `question` tool and only rejects when that is absent too.
27
+ * - leaving the host's own `plan` agent without its `plan_exit` goes through
28
+ * CreatePlan, whose `question` prompt is the execution approval, so the
29
+ * request is rejected with a pointer there (or, with no `question`, to the
30
+ * user's agent switch); a Cursor-only plan mode has nothing on the host to
31
+ * leave and is approved.
32
32
  *
33
33
  * Resolution is keyed solely on the advertised catalog under canonical
34
34
  * OpenCode tool names — never on any external identity or model id.
@@ -36,7 +36,6 @@
36
36
  * CLI reject reason for user declines: "Mode switch rejected by user"
37
37
  * (chunk-7076/dist/ui.js onSwitchModeReject).
38
38
  */
39
- import { type OpencodeQuestionInput } from "./ask-question.js";
40
39
  /**
41
40
  * `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
42
41
  * Continuation writes an InteractionResponse, not an ExecClientMessage.
@@ -58,10 +57,16 @@ export declare const MISSING_TARGET_REASON = "Missing targetModeId";
58
57
  * even though the next Run will succeed. CreatePlan already soft-acks the same
59
58
  * case; SwitchMode must match.
60
59
  */
61
- export declare const PLAN_EXIT_UNAVAILABLE_REASON: string;
62
- /** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */
63
- export declare const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?";
64
- export declare const SWITCH_MODE_EXIT_HEADER = "Build Agent";
60
+ /**
61
+ * The host's plan agent is active and offers no `plan_exit`: on such a host
62
+ * the user ends plan mode by switching agents, so the model cannot.
63
+ */
64
+ export declare const PLAN_EXIT_BY_USER_REASON: string;
65
+ /**
66
+ * The host's plan agent is active, offers no `plan_exit`, but can ask: leaving
67
+ * plan mode goes through CreatePlan, whose `question` prompt is the approval.
68
+ */
69
+ export declare const PLAN_EXIT_VIA_CREATE_PLAN_REASON: string;
65
70
  export type SwitchModeHostTool = "plan_enter" | "plan_exit";
66
71
  /**
67
72
  * How a SwitchMode query is satisfied this turn.
@@ -70,7 +75,6 @@ export type SwitchModeHostTool = "plan_enter" | "plan_exit";
70
75
  * - `approve` entering plan mode needs no host tool — approve immediately.
71
76
  * - `ack` lifecycle turn (`allowTools=false`): approve on the wire without
72
77
  * mutating session mode (same soft-ack pattern as CreatePlan).
73
- * - `question` leaving plan mode falls back to the host `question` prompt.
74
78
  * - `reject` nothing can satisfy it; `reason` names the real cause.
75
79
  */
76
80
  export type SwitchModeBridge = {
@@ -80,9 +84,6 @@ export type SwitchModeBridge = {
80
84
  kind: "approve";
81
85
  } | {
82
86
  kind: "ack";
83
- } | {
84
- kind: "question";
85
- input: OpencodeQuestionInput;
86
87
  } | {
87
88
  kind: "reject";
88
89
  reason: string;
@@ -114,21 +115,22 @@ export declare function normalizeSwitchModeId(targetModeId: string): string;
114
115
  * for CLI-shaped behavioral guidance.
115
116
  */
116
117
  export declare function mapSwitchModeTarget(targetModeId: string): SwitchModeMapping;
117
- /** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
118
- export declare function switchModeExitQuestionInput(): OpencodeQuestionInput;
119
118
  /**
120
119
  * Resolve how this SwitchMode target is satisfied, keyed only on the advertised
121
120
  * catalog under canonical tool names.
122
121
  *
123
- * A missing plan tool is not a refusal: entering plan mode needs no host tool,
124
- * and leaving it degrades to the `question` prompt so the user still approves
125
- * before any execution starts.
122
+ * A missing plan tool is not a refusal: entering plan mode needs no host tool.
123
+ * Leaving the host's own plan agent without `plan_exit` is approved through
124
+ * CreatePlan (or the user's agent switch when nothing can ask), and a
125
+ * Cursor-only plan mode (no host plan agent) has nothing on the host to leave.
126
126
  */
127
127
  export declare function resolveSwitchModeBridge(targetModeId: string, options: {
128
128
  allowTools: boolean;
129
129
  advertised: ReadonlySet<string> | Iterable<string>;
130
130
  /** Cursor unified mode currently recorded for this session, if any. */
131
131
  activeModeId?: string;
132
+ /** Host primary agent of this Run, when the host reported it. */
133
+ hostAgent?: string;
132
134
  }): SwitchModeBridge;
133
135
  /** Decode a `switch_mode_request_query` body, or undefined when unusable. */
134
136
  export declare function decodeSwitchModeQuery(queryBytes: Uint8Array): DecodedSwitchModeQuery | undefined;
@@ -142,12 +144,6 @@ export declare function switchModeRejectedResult(reason: string): Record<string,
142
144
  * Question/permission declines and empty failures use the CLI user-reject string.
143
145
  */
144
146
  export declare function switchModeResultFromToolOutput(output: string, isError: boolean): Record<string, unknown>;
145
- /**
146
- * Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
147
- * plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
148
- * keeps the model in plan mode rather than silently starting execution.
149
- */
150
- export declare function switchModeResultFromQuestionOutput(output: string, isError: boolean): Record<string, unknown>;
151
147
  /** Record the approved Cursor mode for later system-reminder injection. */
152
148
  export declare function setActiveCursorMode(sessionKey: string | undefined, targetModeId: string, options?: {
153
149
  bridgedPlanEntered?: boolean;
@@ -155,8 +151,47 @@ export declare function setActiveCursorMode(sessionKey: string | undefined, targ
155
151
  export declare function getActiveCursorMode(sessionKey: string | undefined): string | undefined;
156
152
  /** True while the provider has an approved Cursor plan/spec mode for this session. */
157
153
  export declare function isCursorPlanModeActive(sessionKey: string | undefined): boolean;
158
- /** True after a bridged plan/spec Run has observed plan_enter removed by the host. */
154
+ /** True while Cursor plan/spec mode is one the host enforces (see `bridgedPlanEntered`). */
159
155
  export declare function isBridgedCursorPlanModeActive(sessionKey: string | undefined): boolean;
156
+ /**
157
+ * Cursor plan mode follows the host's `plan` primary agent, as Cursor CLI's
158
+ * mode follows its own mode switch (Shift+Tab, `/plan`).
159
+ *
160
+ * - A turn under the host plan agent puts Cursor in plan mode, however the
161
+ * host got there (its agent picker, a plan_enter approval).
162
+ * - The host's own exit from that agent (a plan_exit approval, or an agent
163
+ * switch in its UI) ends Cursor plan mode. Only an observed `plan` → other
164
+ * transition counts, so Cursor-only modes (debug, ask, …) under the host's
165
+ * ordinary agent are kept; the next Run then carries the agent-mode
166
+ * reminder instead of a stale "plan mode is still active" one.
167
+ */
168
+ export declare function followHostPlanAgent(sessionKey: string | undefined, previousHostAgent: string | undefined, hostAgent: string | undefined): "entered" | "left" | undefined;
169
+ /**
170
+ * `agent.v1.AgentMode` for a Cursor mode id, as Cursor CLI derives
171
+ * `UserMessage.mode` from its current mode: plan, ask, and debug are their own
172
+ * modes; every other mode (agent, spec, triage, …) runs as agent.
173
+ */
174
+ export declare function cursorAgentModeWireValue(modeId: string | undefined): number;
175
+ /**
176
+ * One-shot translation of the host plan agent's workflow into Cursor terms,
177
+ * delivered once per Cursor conversation while that agent owns the turn.
178
+ *
179
+ * With the host's plan file known, Cursor's native CreatePlan carries out the
180
+ * host instruction "write the plan file, then call plan_exit" (see the `exit`
181
+ * CreatePlan bridge), so the model needs no host tool at all. Without it, the
182
+ * model is given `plan_exit`'s exact call shape from the live schema: the
183
+ * RequestContext overlay is names-only, and otherwise it must look the tool up
184
+ * (or guess its arguments) before it can submit the plan.
185
+ */
186
+ export declare function takeHostPlanAgentNote(sessionKey: string | undefined, conversationId: string, hostAgent: string | undefined, tools: ReadonlyArray<{
187
+ name: string;
188
+ inputSchema?: unknown;
189
+ }>, options?: {
190
+ /** The host's own plan file for this session, when the host defines one. */
191
+ hostPlanFile?: string;
192
+ /** MCP server identity the advertised `plan_exit` is called through. */
193
+ server?: string;
194
+ }): string | undefined;
160
195
  export declare function clearActiveCursorMode(sessionKey: string | undefined): void;
161
196
  export declare function resetActiveCursorModesForTests(): void;
162
197
  /**
@@ -168,6 +203,8 @@ export declare function cursorModeSystemReminder(targetModeId: string, options?:
168
203
  firstTurn?: boolean;
169
204
  planExitAdvertised?: boolean;
170
205
  planStageAdvertised?: boolean;
206
+ /** The host `question` tool is advertised, so CreatePlan asks to start implementing. */
207
+ questionAdvertised?: boolean;
171
208
  }): string | undefined;
172
209
  /**
173
210
  * Consume the active-mode reminder for this OpenCode session.
@@ -175,4 +212,6 @@ export declare function cursorModeSystemReminder(targetModeId: string, options?:
175
212
  */
176
213
  export declare function takeActiveCursorModeReminder(sessionKey: string | undefined, options?: {
177
214
  advertisedTools?: ReadonlySet<string> | Iterable<string>;
215
+ /** Host primary agent of this request; when known it, not the catalog, says whether planning ended. */
216
+ hostAgent?: string;
178
217
  }): string | undefined;