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.
- package/CHANGELOG.md +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- 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
|
|
16
|
-
* Tool outputs, when required for
|
|
17
|
-
* user-role OpenCode host
|
|
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,
|
|
24
|
-
* state
|
|
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
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* so Cursor
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
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;
|
package/dist/protocol/request.js
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
import { encodeMessage, getMessageTypes } from "./messages.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* so Cursor
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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(
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
67
|
-
?
|
|
68
|
-
:
|
|
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`).
|
|
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
|
|
31
|
-
*
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
|
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;
|