cursor-opencode-provider 0.7.6 → 0.8.1
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 +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- 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/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- 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 +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- 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 +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/protocol/request.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { encodeMessage, getMessageTypes } from "./messages.js";
|
|
2
|
-
import { toolsToDescriptors } from "./tools.js";
|
|
3
2
|
/**
|
|
4
3
|
* Seed ConversationStateStructure for the first turn (no checkpoint yet).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
15
|
*
|
|
13
16
|
* We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
|
|
14
17
|
* field is the internal `--system-prompt` CLI override and the server rejects
|
|
@@ -18,14 +21,8 @@ export function buildSeedConversationState(input) {
|
|
|
18
21
|
const root = getMessageTypes();
|
|
19
22
|
const type = root.lookupType("ConversationStateStructure");
|
|
20
23
|
const messages = [];
|
|
21
|
-
if (input?.systemPrompt && input.systemPrompt.length > 0) {
|
|
22
|
-
messages.push(JSON.stringify({ role: "system", content: input.systemPrompt }));
|
|
23
|
-
}
|
|
24
24
|
for (const entry of input?.history ?? []) {
|
|
25
|
-
if (!entry.content)
|
|
26
|
-
continue;
|
|
27
|
-
// Avoid duplicating the system prompt when history also carries one.
|
|
28
|
-
if (entry.role === "system" && input?.systemPrompt)
|
|
25
|
+
if (!entry.content || entry.role === "system")
|
|
29
26
|
continue;
|
|
30
27
|
messages.push(JSON.stringify({ role: entry.role, content: entry.content }));
|
|
31
28
|
}
|
|
@@ -42,16 +39,16 @@ export function buildSeedConversationState(input) {
|
|
|
42
39
|
*/
|
|
43
40
|
export function buildRunRequest(input) {
|
|
44
41
|
const msgId = input.messageId ?? crypto.randomUUID();
|
|
45
|
-
// Advertise
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
const tools = input.tools ?? [];
|
|
49
|
-
const mcpTools = input.toolDescriptors ?? (tools.length > 0 ? toolsToDescriptors(tools) : []);
|
|
42
|
+
// Advertise host tools on UserMessageAction.request_context (#2) via slim
|
|
43
|
+
// mcp_meta_tool_options. AgentRunRequest.mcp_tools (#4) stays empty on real
|
|
44
|
+
// turns (CLI prewarm-only). Full defs are session.toolDescriptors + exec #36.
|
|
50
45
|
const requestContext = input.requestContext;
|
|
51
46
|
const userMessage = {
|
|
52
47
|
text: input.text,
|
|
53
48
|
message_id: msgId,
|
|
54
49
|
};
|
|
50
|
+
if (input.mode !== undefined)
|
|
51
|
+
userMessage.mode = input.mode;
|
|
55
52
|
if (input.images?.length) {
|
|
56
53
|
userMessage.selected_context = {
|
|
57
54
|
selected_images: input.images.map((image) => ({
|
|
@@ -70,10 +67,7 @@ export function buildRunRequest(input) {
|
|
|
70
67
|
: { user_message_action: userMessageAction };
|
|
71
68
|
const conversationState = input.conversationState && input.conversationState.length > 0
|
|
72
69
|
? input.conversationState
|
|
73
|
-
: buildSeedConversationState({
|
|
74
|
-
systemPrompt: input.systemPrompt,
|
|
75
|
-
history: input.history,
|
|
76
|
-
});
|
|
70
|
+
: buildSeedConversationState({ history: input.history });
|
|
77
71
|
const runRequest = {
|
|
78
72
|
conversation_id: input.conversationId,
|
|
79
73
|
conversation_group_id: input.conversationGroupId ?? input.conversationId,
|
|
@@ -88,9 +82,7 @@ export function buildRunRequest(input) {
|
|
|
88
82
|
parameters: input.parameterValues ?? [],
|
|
89
83
|
},
|
|
90
84
|
conversation_state: conversationState,
|
|
91
|
-
|
|
92
|
-
// older server builds that still read it).
|
|
93
|
-
mcp_tools: { mcp_tools: mcpTools },
|
|
85
|
+
mcp_tools: { mcp_tools: [] },
|
|
94
86
|
unknown_flag: 0,
|
|
95
87
|
field_12: 0,
|
|
96
88
|
};
|
|
@@ -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;
|
|
@@ -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,7 @@
|
|
|
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 {
|
|
39
|
+
import { CURSOR_PLAN_STAGE_TOOL } from "./create-plan.js";
|
|
40
40
|
import { decodeMessageSparse } from "./messages.js";
|
|
41
41
|
/**
|
|
42
42
|
* `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
|
|
@@ -59,13 +59,18 @@ export const MISSING_TARGET_REASON = "Missing targetModeId";
|
|
|
59
59
|
* even though the next Run will succeed. CreatePlan already soft-acks the same
|
|
60
60
|
* case; SwitchMode must match.
|
|
61
61
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
export const
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The host's plan agent is active and offers no `plan_exit`: on such a host
|
|
64
|
+
* the user ends plan mode by switching agents, so the model cannot.
|
|
65
|
+
*/
|
|
66
|
+
export const PLAN_EXIT_BY_USER_REASON = "This host leaves plan mode when the user switches from the plan agent to the build " +
|
|
67
|
+
"agent. Ask the user to switch agents when the plan is ready; do not implement before then.";
|
|
68
|
+
/**
|
|
69
|
+
* The host's plan agent is active, offers no `plan_exit`, but can ask: leaving
|
|
70
|
+
* plan mode goes through CreatePlan, whose `question` prompt is the approval.
|
|
71
|
+
*/
|
|
72
|
+
export const PLAN_EXIT_VIA_CREATE_PLAN_REASON = "Leave plan mode by recording the finished plan with Cursor CreatePlan: the user is then " +
|
|
73
|
+
"asked whether to switch to the build agent and start implementing. Do not implement before they approve.";
|
|
69
74
|
const activeCursorModeBySession = new Map();
|
|
70
75
|
const MAX_ACTIVE_CURSOR_MODES = 256;
|
|
71
76
|
function asRecord(value) {
|
|
@@ -97,46 +102,14 @@ export function mapSwitchModeTarget(targetModeId) {
|
|
|
97
102
|
}
|
|
98
103
|
return { ok: true, toolName: "plan_exit" };
|
|
99
104
|
}
|
|
100
|
-
/** The synthetic AskQuestion item backing a question-emulated plan exit. */
|
|
101
|
-
function switchModeExitQuestionItem() {
|
|
102
|
-
return {
|
|
103
|
-
id: "switch_mode_exit",
|
|
104
|
-
prompt: SWITCH_MODE_EXIT_QUESTION,
|
|
105
|
-
options: [
|
|
106
|
-
{ id: "yes", label: SWITCH_MODE_EXIT_YES },
|
|
107
|
-
{ id: "no", label: SWITCH_MODE_EXIT_NO },
|
|
108
|
-
],
|
|
109
|
-
allowMultiple: false,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
|
|
113
|
-
export function switchModeExitQuestionInput() {
|
|
114
|
-
return {
|
|
115
|
-
questions: [
|
|
116
|
-
{
|
|
117
|
-
question: SWITCH_MODE_EXIT_QUESTION,
|
|
118
|
-
header: SWITCH_MODE_EXIT_HEADER,
|
|
119
|
-
options: [
|
|
120
|
-
{
|
|
121
|
-
label: SWITCH_MODE_EXIT_YES,
|
|
122
|
-
description: "Switch to build agent and start implementing the plan",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
label: SWITCH_MODE_EXIT_NO,
|
|
126
|
-
description: "Stay with plan agent to continue refining the plan",
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
105
|
/**
|
|
134
106
|
* Resolve how this SwitchMode target is satisfied, keyed only on the advertised
|
|
135
107
|
* catalog under canonical tool names.
|
|
136
108
|
*
|
|
137
|
-
* A missing plan tool is not a refusal: entering plan mode needs no host tool
|
|
138
|
-
*
|
|
139
|
-
*
|
|
109
|
+
* A missing plan tool is not a refusal: entering plan mode needs no host tool.
|
|
110
|
+
* Leaving the host's own plan agent without `plan_exit` is approved through
|
|
111
|
+
* CreatePlan (or the user's agent switch when nothing can ask), and a
|
|
112
|
+
* Cursor-only plan mode (no host plan agent) has nothing on the host to leave.
|
|
140
113
|
*/
|
|
141
114
|
export function resolveSwitchModeBridge(targetModeId, options) {
|
|
142
115
|
const mapped = mapSwitchModeTarget(targetModeId);
|
|
@@ -162,11 +135,13 @@ export function resolveSwitchModeBridge(targetModeId, options) {
|
|
|
162
135
|
// whole contract, exactly as Cursor CLI's own plan mode is prompt-enforced.
|
|
163
136
|
if (mapped.toolName === "plan_enter")
|
|
164
137
|
return { kind: "approve" };
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
if (options.hostAgent === "plan") {
|
|
139
|
+
return {
|
|
140
|
+
kind: "reject",
|
|
141
|
+
reason: names.has("question") ? PLAN_EXIT_VIA_CREATE_PLAN_REASON : PLAN_EXIT_BY_USER_REASON,
|
|
142
|
+
};
|
|
168
143
|
}
|
|
169
|
-
return { kind: "
|
|
144
|
+
return { kind: "approve" };
|
|
170
145
|
}
|
|
171
146
|
/** Decode a `switch_mode_request_query` body, or undefined when unusable. */
|
|
172
147
|
export function decodeSwitchModeQuery(queryBytes) {
|
|
@@ -222,20 +197,6 @@ export function switchModeResultFromToolOutput(output, isError) {
|
|
|
222
197
|
}
|
|
223
198
|
return switchModeRejectedResult(trimmed);
|
|
224
199
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
|
|
227
|
-
* plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
|
|
228
|
-
* keeps the model in plan mode rather than silently starting execution.
|
|
229
|
-
*/
|
|
230
|
-
export function switchModeResultFromQuestionOutput(output, isError) {
|
|
231
|
-
if (isError)
|
|
232
|
-
return switchModeResultFromToolOutput(output, true);
|
|
233
|
-
const [segment] = parseAnswerSegments([switchModeExitQuestionItem()], output);
|
|
234
|
-
const answer = (segment ?? "").trim().toLowerCase();
|
|
235
|
-
if (answer === SWITCH_MODE_EXIT_YES.toLowerCase())
|
|
236
|
-
return switchModeApprovedResult();
|
|
237
|
-
return switchModeRejectedResult(USER_REJECTED_REASON);
|
|
238
|
-
}
|
|
239
200
|
/** Record the approved Cursor mode for later system-reminder injection. */
|
|
240
201
|
export function setActiveCursorMode(sessionKey, targetModeId, options = {}) {
|
|
241
202
|
if (!sessionKey)
|
|
@@ -265,13 +226,121 @@ export function isCursorPlanModeActive(sessionKey) {
|
|
|
265
226
|
const mode = getActiveCursorMode(sessionKey);
|
|
266
227
|
return mode === "plan" || mode === "spec";
|
|
267
228
|
}
|
|
268
|
-
/** True
|
|
229
|
+
/** True while Cursor plan/spec mode is one the host enforces (see `bridgedPlanEntered`). */
|
|
269
230
|
export function isBridgedCursorPlanModeActive(sessionKey) {
|
|
270
231
|
if (!sessionKey)
|
|
271
232
|
return false;
|
|
272
233
|
const state = activeCursorModeBySession.get(sessionKey);
|
|
273
234
|
return (state?.modeId === "plan" || state?.modeId === "spec") && state.bridgedPlanEntered;
|
|
274
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Cursor plan mode follows the host's `plan` primary agent, as Cursor CLI's
|
|
238
|
+
* mode follows its own mode switch (Shift+Tab, `/plan`).
|
|
239
|
+
*
|
|
240
|
+
* - A turn under the host plan agent puts Cursor in plan mode, however the
|
|
241
|
+
* host got there (its agent picker, a plan_enter approval).
|
|
242
|
+
* - The host's own exit from that agent (a plan_exit approval, or an agent
|
|
243
|
+
* switch in its UI) ends Cursor plan mode. Only an observed `plan` → other
|
|
244
|
+
* transition counts, so Cursor-only modes (debug, ask, …) under the host's
|
|
245
|
+
* ordinary agent are kept; the next Run then carries the agent-mode
|
|
246
|
+
* reminder instead of a stale "plan mode is still active" one.
|
|
247
|
+
*/
|
|
248
|
+
export function followHostPlanAgent(sessionKey, previousHostAgent, hostAgent) {
|
|
249
|
+
if (!sessionKey || !hostAgent)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (hostAgent === "plan") {
|
|
252
|
+
if (isBridgedCursorPlanModeActive(sessionKey))
|
|
253
|
+
return undefined;
|
|
254
|
+
// The host runs its own plan agent, so it enforces this plan mode.
|
|
255
|
+
setActiveCursorMode(sessionKey, "plan", { bridgedPlanEntered: true });
|
|
256
|
+
return "entered";
|
|
257
|
+
}
|
|
258
|
+
if (previousHostAgent !== "plan" || !isCursorPlanModeActive(sessionKey))
|
|
259
|
+
return undefined;
|
|
260
|
+
setActiveCursorMode(sessionKey, "agent");
|
|
261
|
+
return "left";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* `agent.v1.AgentMode` for a Cursor mode id, as Cursor CLI derives
|
|
265
|
+
* `UserMessage.mode` from its current mode: plan, ask, and debug are their own
|
|
266
|
+
* modes; every other mode (agent, spec, triage, …) runs as agent.
|
|
267
|
+
*/
|
|
268
|
+
export function cursorAgentModeWireValue(modeId) {
|
|
269
|
+
switch (normalizeSwitchModeId(modeId ?? "")) {
|
|
270
|
+
case "plan":
|
|
271
|
+
return 3;
|
|
272
|
+
case "chat":
|
|
273
|
+
case "ask":
|
|
274
|
+
case "search":
|
|
275
|
+
return 2;
|
|
276
|
+
case "debug":
|
|
277
|
+
return 4;
|
|
278
|
+
default:
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Conversation that already received the host plan-exit call shape, per session. */
|
|
283
|
+
const hostPlanExitNoteBySession = new Map();
|
|
284
|
+
function describeToolArguments(schema) {
|
|
285
|
+
const record = asRecord(schema);
|
|
286
|
+
const properties = asRecord(record?.properties);
|
|
287
|
+
const names = properties ? Object.keys(properties) : [];
|
|
288
|
+
if (names.length === 0)
|
|
289
|
+
return "It takes no arguments: call it with `{}`.";
|
|
290
|
+
const required = new Set(Array.isArray(record?.required) ? record.required.filter((name) => typeof name === "string") : []);
|
|
291
|
+
const lines = names.map((name) => {
|
|
292
|
+
const property = asRecord(properties[name]);
|
|
293
|
+
const type = str(property?.type) || "value";
|
|
294
|
+
const description = str(property?.description).trim();
|
|
295
|
+
return `- \`${name}\` (${type}, ${required.has(name) ? "required" : "optional"})` +
|
|
296
|
+
(description ? `: ${description}` : "");
|
|
297
|
+
});
|
|
298
|
+
return `Its arguments (use exactly these names):\n${lines.join("\n")}`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* One-shot translation of the host plan agent's workflow into Cursor terms,
|
|
302
|
+
* delivered once per Cursor conversation while that agent owns the turn.
|
|
303
|
+
*
|
|
304
|
+
* With the host's plan file known, Cursor's native CreatePlan carries out the
|
|
305
|
+
* host instruction "write the plan file, then call plan_exit" (see the `exit`
|
|
306
|
+
* CreatePlan bridge), so the model needs no host tool at all. Without it, the
|
|
307
|
+
* model is given `plan_exit`'s exact call shape from the live schema: the
|
|
308
|
+
* RequestContext overlay is names-only, and otherwise it must look the tool up
|
|
309
|
+
* (or guess its arguments) before it can submit the plan.
|
|
310
|
+
*/
|
|
311
|
+
export function takeHostPlanAgentNote(sessionKey, conversationId, hostAgent, tools, options = {}) {
|
|
312
|
+
if (!sessionKey)
|
|
313
|
+
return undefined;
|
|
314
|
+
if (hostAgent !== "plan") {
|
|
315
|
+
hostPlanExitNoteBySession.delete(sessionKey);
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const planExit = tools.find((tool) => tool.name === "plan_exit");
|
|
319
|
+
// A host plan-stage tool carries the plan to the host review itself.
|
|
320
|
+
if (tools.some((tool) => tool.name === CURSOR_PLAN_STAGE_TOOL))
|
|
321
|
+
return undefined;
|
|
322
|
+
if (!planExit || hostPlanExitNoteBySession.get(sessionKey) === conversationId)
|
|
323
|
+
return undefined;
|
|
324
|
+
hostPlanExitNoteBySession.delete(sessionKey);
|
|
325
|
+
hostPlanExitNoteBySession.set(sessionKey, conversationId);
|
|
326
|
+
while (hostPlanExitNoteBySession.size > MAX_ACTIVE_CURSOR_MODES) {
|
|
327
|
+
const oldest = hostPlanExitNoteBySession.keys().next().value;
|
|
328
|
+
if (!oldest)
|
|
329
|
+
break;
|
|
330
|
+
hostPlanExitNoteBySession.delete(oldest);
|
|
331
|
+
}
|
|
332
|
+
if (options.hostPlanFile) {
|
|
333
|
+
return wrapReminder("The host plan agent is active. When the plan is ready, record it with CreatePlan. " +
|
|
334
|
+
`CreatePlan saves it as the host's plan file (${options.hostPlanFile}) and submits it ` +
|
|
335
|
+
"to the host's plan review, which asks the user; this carries out the host " +
|
|
336
|
+
"instructions to write the plan file and call plan_exit, so do neither yourself. " +
|
|
337
|
+
"Do not implement until the user approves.");
|
|
338
|
+
}
|
|
339
|
+
return wrapReminder("The host plan agent is active. When the plan file is written and ready for review, " +
|
|
340
|
+
`submit it by calling MCP tool \`plan_exit\` on server \`${options.server ?? "opencode"}\` directly. ` +
|
|
341
|
+
"Its full definition is below, so do not look it up with GetMcpTools first. " +
|
|
342
|
+
describeToolArguments(planExit.inputSchema));
|
|
343
|
+
}
|
|
275
344
|
export function clearActiveCursorMode(sessionKey) {
|
|
276
345
|
if (!sessionKey)
|
|
277
346
|
return;
|
|
@@ -279,6 +348,7 @@ export function clearActiveCursorMode(sessionKey) {
|
|
|
279
348
|
}
|
|
280
349
|
export function resetActiveCursorModesForTests() {
|
|
281
350
|
activeCursorModeBySession.clear();
|
|
351
|
+
hostPlanExitNoteBySession.clear();
|
|
282
352
|
}
|
|
283
353
|
function wrapReminder(body) {
|
|
284
354
|
return `<system_reminder>\n${body.trim()}\n</system_reminder>`;
|
|
@@ -295,12 +365,15 @@ export function cursorModeSystemReminder(targetModeId, options = {}) {
|
|
|
295
365
|
const first = options.firstTurn !== false;
|
|
296
366
|
// A host plan-stage tool writes the artifact and its result names the
|
|
297
367
|
// follow-up that requests approval. plan_exit only leaves plan mode.
|
|
298
|
-
// Without either
|
|
299
|
-
//
|
|
368
|
+
// Without either, CreatePlan asks through `question` whether to implement;
|
|
369
|
+
// with no `question` either, the user leaves plan mode by switching agents.
|
|
370
|
+
// Naming an unavailable tool would strand the model.
|
|
300
371
|
const leavePlan = options.planStageAdvertised
|
|
301
372
|
? "record the finished plan with Cursor CreatePlan. The host stage tool waits for the host plan review and does not return until the user accepts or declines. Do not call `plan_exit` to submit or skip that review, and do not implement until the tool returns success"
|
|
302
373
|
: options.planExitAdvertised === false
|
|
303
|
-
?
|
|
374
|
+
? options.questionAdvertised
|
|
375
|
+
? "record the finished plan with Cursor CreatePlan. Writing it needs no approval; the user is then asked whether to switch to the build agent and start implementing. If they decline, refine the plan and record it again"
|
|
376
|
+
: "record the finished plan (Cursor CreatePlan), then tell the user it is ready: they switch to the build agent to have it implemented"
|
|
304
377
|
: "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
|
|
305
378
|
if (id === "plan" || id === "spec") {
|
|
306
379
|
return wrapReminder(first
|
|
@@ -413,7 +486,7 @@ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
|
|
|
413
486
|
: new Set(options.advertisedTools)
|
|
414
487
|
: undefined;
|
|
415
488
|
const isPlanMode = state.modeId === "plan" || state.modeId === "spec";
|
|
416
|
-
if (isPlanMode && state.bridgedPlanEntered && advertised) {
|
|
489
|
+
if (isPlanMode && state.bridgedPlanEntered && advertised && !options.hostAgent) {
|
|
417
490
|
if (advertised.has("plan_enter")) {
|
|
418
491
|
activeCursorModeBySession.delete(sessionKey);
|
|
419
492
|
return cursorModeSystemReminder("agent", { firstTurn: true });
|
|
@@ -425,6 +498,7 @@ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
|
|
|
425
498
|
? {
|
|
426
499
|
planExitAdvertised: advertised.has("plan_exit"),
|
|
427
500
|
planStageAdvertised: advertised.has("cursor_plan_stage"),
|
|
501
|
+
questionAdvertised: advertised.has("question"),
|
|
428
502
|
}
|
|
429
503
|
: {}),
|
|
430
504
|
});
|
|
@@ -15,6 +15,8 @@ export type CursorConversationTokenDetails = {
|
|
|
15
15
|
breakdown?: CursorPromptTokenBreakdown;
|
|
16
16
|
};
|
|
17
17
|
export type CursorContextUsageSource = "checkpoint-current-run" | "checkpoint-previous-turn";
|
|
18
|
+
/** Cursor can update occupancy while retaining categories from an older checkpoint. */
|
|
19
|
+
export declare function currentCursorTokenBreakdown(details: CursorConversationTokenDetails | undefined): CursorPromptTokenBreakdown | undefined;
|
|
18
20
|
/**
|
|
19
21
|
* Read field #5 (`ConversationTokenDetails`) from Cursor's opaque
|
|
20
22
|
* `ConversationStateStructure` checkpoint without decoding or re-encoding the
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import protobuf from "protobufjs";
|
|
2
|
+
/** Cursor can update occupancy while retaining categories from an older checkpoint. */
|
|
3
|
+
export function currentCursorTokenBreakdown(details) {
|
|
4
|
+
const breakdown = details?.breakdown;
|
|
5
|
+
return breakdown
|
|
6
|
+
&& breakdown.totalUsedTokens === details.usedTokens
|
|
7
|
+
&& breakdown.maxTokens === details.maxTokens
|
|
8
|
+
? breakdown
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
2
11
|
const MAX_BREAKDOWN_CATEGORIES = 128;
|
|
3
12
|
function requireWireType(actual, expected, field) {
|
|
4
13
|
if (actual !== expected)
|
|
@@ -142,6 +151,7 @@ export function cursorContextUsageMetadata(details, source = "checkpoint-current
|
|
|
142
151
|
const usedPercent = details.maxTokens > 0
|
|
143
152
|
? Math.max(0, Math.min(100, Math.round(details.usedTokens / details.maxTokens * 1_000) / 10))
|
|
144
153
|
: undefined;
|
|
154
|
+
const breakdown = currentCursorTokenBreakdown(details);
|
|
145
155
|
return {
|
|
146
156
|
contextUsageVersion: 2,
|
|
147
157
|
source,
|
|
@@ -150,6 +160,6 @@ export function cursorContextUsageMetadata(details, source = "checkpoint-current
|
|
|
150
160
|
maxTokens: details.maxTokens,
|
|
151
161
|
remainingTokens: Math.max(0, details.maxTokens - details.usedTokens),
|
|
152
162
|
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
153
|
-
...(
|
|
163
|
+
...(breakdown ? { breakdown } : {}),
|
|
154
164
|
};
|
|
155
165
|
}
|
|
@@ -22,6 +22,10 @@ export type BridgedOpenCodeToolCall = {
|
|
|
22
22
|
};
|
|
23
23
|
/** Display variants Cursor executes itself (mcp_state + optional write spill). */
|
|
24
24
|
export declare function isNativeDisplayToolCall(variant: string): boolean;
|
|
25
|
+
/** Server-side dynamic-call failures may complete without a started/exec frame. */
|
|
26
|
+
export declare function displayMcpToolError(toolCall: Record<string, unknown> | undefined): string | undefined;
|
|
27
|
+
/** Native discovery errors are server-side results, never host execution. */
|
|
28
|
+
export declare function displayNativeDiscoveryError(toolCall: Record<string, unknown> | undefined): string | undefined;
|
|
25
29
|
/**
|
|
26
30
|
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
27
31
|
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
@@ -69,6 +69,20 @@ export function isNativeDisplayToolCall(variant) {
|
|
|
69
69
|
function asRecord(v) {
|
|
70
70
|
return v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
71
71
|
}
|
|
72
|
+
/** Server-side dynamic-call failures may complete without a started/exec frame. */
|
|
73
|
+
export function displayMcpToolError(toolCall) {
|
|
74
|
+
const call = asRecord(toolCall?.mcp_tool_call);
|
|
75
|
+
const result = asRecord(call?.result);
|
|
76
|
+
const error = asRecord(result?.error);
|
|
77
|
+
const parts = [error?.error, error?.detail].filter((value) => typeof value === "string" && value.length > 0);
|
|
78
|
+
return parts.length > 0 ? parts.join(": ") : undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Native discovery errors are server-side results, never host execution. */
|
|
81
|
+
export function displayNativeDiscoveryError(toolCall) {
|
|
82
|
+
const call = asRecord(toolCall?.get_mcp_tools_tool_call);
|
|
83
|
+
const error = asRecord(asRecord(call?.result)?.error)?.error;
|
|
84
|
+
return typeof error === "string" && error.length > 0 ? error : undefined;
|
|
85
|
+
}
|
|
72
86
|
function findToolVariant(toolCall) {
|
|
73
87
|
for (const key of TOOL_CALL_VARIANTS) {
|
|
74
88
|
if (toolCall[key] != null)
|