cursor-opencode-provider 0.7.6 → 0.8.0
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/README.md +9 -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 +42 -5
- package/dist/context/build.js +96 -92
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +10 -160
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +37 -14
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +7 -4
- package/dist/context/paths.js +13 -9
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- 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.js +2 -2
- package/dist/image-save.js +3 -3
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +41 -8
- package/dist/language-model.js +391 -98
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/plan-execution-kickoff.js +2 -2
- package/dist/plugin-opencode2.js +10 -16
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +138 -109
- 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.js +2 -1
- package/dist/protocol/messages.js +22 -4
- package/dist/protocol/request.d.ts +11 -9
- package/dist/protocol/request.js +15 -25
- package/dist/protocol/tools.d.ts +17 -10
- package/dist/protocol/tools.js +53 -93
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/session.d.ts +9 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/package.json +1 -1
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/context/skills.d.ts +0 -11
- package/dist/context/skills.js +0 -110
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
export type AgentSkillLike = {
|
|
2
|
-
/** OpenCode skill id (frontmatter `name`, else directory name). */
|
|
3
|
-
id?: unknown;
|
|
4
|
-
full_path?: unknown;
|
|
5
|
-
description?: unknown;
|
|
6
|
-
content?: unknown;
|
|
7
|
-
};
|
|
8
1
|
/**
|
|
9
2
|
* Configured MCP servers that own at least one advertised tool. Uses the same
|
|
10
3
|
* server resolution as the RequestContext descriptors, so guidance never names
|
|
@@ -12,63 +5,14 @@ export type AgentSkillLike = {
|
|
|
12
5
|
* `sourceName` when present).
|
|
13
6
|
*/
|
|
14
7
|
export declare function listAdvertisedMcpServers(toolNames: Iterable<string>, knownMcpServers?: Iterable<string>): string[];
|
|
15
|
-
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
16
|
-
export declare function skillNameFromAgentSkill(skill: AgentSkillLike): string | undefined;
|
|
17
8
|
/**
|
|
18
9
|
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
19
10
|
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
20
|
-
* stay out of the frozen baseline (they live in
|
|
21
|
-
* and in
|
|
11
|
+
* stay out of the frozen baseline (they live in the host system prompt / skill
|
|
12
|
+
* tool, and in host `<system-update>` when the catalog changes — same split as
|
|
22
13
|
* OpenCode's SkillGuidance / SkillInstructions).
|
|
23
14
|
*/
|
|
24
15
|
export declare function buildDynamicCatalogRoutingInstruction(options: {
|
|
25
16
|
toolNames: Iterable<string>;
|
|
26
17
|
knownMcpServers?: Iterable<string>;
|
|
27
18
|
}): string | undefined;
|
|
28
|
-
/**
|
|
29
|
-
* OpenCode-shaped Mid-Conversation skill update.
|
|
30
|
-
*
|
|
31
|
-
* Pure: compare `previousSkillIds` (null = not yet admitted this conversation)
|
|
32
|
-
* to the current catalog. First admission returns undefined — baseline
|
|
33
|
-
* RequestContext + routing instruction already carry the list. Later turns
|
|
34
|
-
* only speak when the id set changes (agent switch / discovery growth), matching
|
|
35
|
-
* `SkillGuidance.update` / `SkillInstructions.update`.
|
|
36
|
-
*/
|
|
37
|
-
export declare function buildSkillCatalogChangeReminder(options: {
|
|
38
|
-
hasSkillTool: boolean;
|
|
39
|
-
skills: readonly AgentSkillLike[];
|
|
40
|
-
/** Sorted skill ids already admitted; `null` means first admission this conversation. */
|
|
41
|
-
previousSkillIds: readonly string[] | null;
|
|
42
|
-
/**
|
|
43
|
-
* Host skill parameter key from the advertised schema.
|
|
44
|
-
* OpenCode 1.x: `name`. OpenCode 2.0: `id`.
|
|
45
|
-
*/
|
|
46
|
-
skillArgKey?: "name" | "id";
|
|
47
|
-
}): {
|
|
48
|
-
text: string | undefined;
|
|
49
|
-
nextSkillIds: string[];
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Admit the current skill catalog for a conversation and return a Mid-Conversation
|
|
53
|
-
* reminder only when OpenCode would (catalog changed after baseline admission).
|
|
54
|
-
*/
|
|
55
|
-
export declare function takeSkillCatalogChangeReminder(conversationId: string, options: {
|
|
56
|
-
hasSkillTool: boolean;
|
|
57
|
-
skills: readonly AgentSkillLike[];
|
|
58
|
-
skillArgKey?: "name" | "id";
|
|
59
|
-
}): string | undefined;
|
|
60
|
-
export declare function clearSkillCatalogAdmission(conversationId: string): void;
|
|
61
|
-
export declare function transferSkillCatalogAdmission(previousConversationId: string, nextConversationId: string): void;
|
|
62
|
-
export declare function resetSkillCatalogAdmissionsForTests(): void;
|
|
63
|
-
/**
|
|
64
|
-
* @deprecated Prefer `buildSkillCatalogChangeReminder` / `takeSkillCatalogChangeReminder`.
|
|
65
|
-
* Kept as a thin alias for callers that still expect the old name; always returns
|
|
66
|
-
* undefined for userText-based matching (OpenCode never did per-turn id nudges).
|
|
67
|
-
*/
|
|
68
|
-
export declare function buildSkillCatalogNudge(options: {
|
|
69
|
-
hasSkillTool: boolean;
|
|
70
|
-
skills: readonly AgentSkillLike[];
|
|
71
|
-
userText?: string;
|
|
72
|
-
skillArgKey?: "name" | "id";
|
|
73
|
-
previousSkillIds?: readonly string[] | null;
|
|
74
|
-
}): string | undefined;
|
|
@@ -2,15 +2,14 @@ import { resolveToolServerIdentity } from "../protocol/tools.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Issue #29: Cursor keeps OpenCode `skill` and MCP tools off its native
|
|
4
4
|
* top-level function list. Both OpenCode 1.x and OpenCode 2.0 reach the model
|
|
5
|
-
* as
|
|
6
|
-
* (exec `mcp_args`). This module builds the shared routing
|
|
7
|
-
*
|
|
5
|
+
* as names in RequestContext `mcp_meta_tool_options`, then GetDynamicTools /
|
|
6
|
+
* CallDynamicTool (exec `mcp_args`). This module builds the shared routing
|
|
7
|
+
* guidance for both hosts.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* - OpenCode 2: skips re-invoke when `<skill_content>` is already in the turn
|
|
9
|
+
* Skill names and descriptions are not repeated here: OpenCode puts them in
|
|
10
|
+
* its own system prompt (`SystemPrompt.skills` / `SkillGuidance` /
|
|
11
|
+
* `SkillInstructions`) and announces catalog changes itself (OpenCode 2
|
|
12
|
+
* `<system-update>`).
|
|
14
13
|
*
|
|
15
14
|
* OpenCode 2 still needs `exposeDirectMcpTools` so MCP tools leave Code Mode
|
|
16
15
|
* and enter the AI SDK catalog; without that step they never reach this
|
|
@@ -19,12 +18,6 @@ import { resolveToolServerIdentity } from "../protocol/tools.js";
|
|
|
19
18
|
const MAX_MCP_SERVERS_IN_GUIDANCE = 8;
|
|
20
19
|
/** Synthetic server builtins and unknown tools are advertised under (`toolsToMcpDescriptors`). */
|
|
21
20
|
const DEFAULT_TOOL_SERVER = "opencode";
|
|
22
|
-
/**
|
|
23
|
-
* Skill ids already admitted to the model for a conversation (baseline RequestContext
|
|
24
|
-
* freeze, or the last Mid-Conversation catalog update). First admission is silent —
|
|
25
|
-
* OpenCode puts that list in the system baseline, not a per-turn reminder.
|
|
26
|
-
*/
|
|
27
|
-
const admittedSkillIdsByConversation = new Map();
|
|
28
21
|
function listWithOverflow(items, limit) {
|
|
29
22
|
const shown = items.slice(0, limit).map((item) => `\`${item}\``).join(", ");
|
|
30
23
|
const hidden = items.length - limit;
|
|
@@ -48,42 +41,11 @@ export function listAdvertisedMcpServers(toolNames, knownMcpServers = []) {
|
|
|
48
41
|
}
|
|
49
42
|
return [...servers];
|
|
50
43
|
}
|
|
51
|
-
/** Skill id OpenCode's `skill` tool expects: the discovered id, else `…/skills/<id>/SKILL.md`. */
|
|
52
|
-
export function skillNameFromAgentSkill(skill) {
|
|
53
|
-
const id = typeof skill.id === "string" ? skill.id.trim() : "";
|
|
54
|
-
if (id)
|
|
55
|
-
return id;
|
|
56
|
-
const fullPath = typeof skill.full_path === "string" ? skill.full_path.trim() : "";
|
|
57
|
-
const parts = fullPath.split(/[\\/]+/).filter(Boolean);
|
|
58
|
-
if (parts.length < 2 || parts.at(-1).toLowerCase() !== "skill.md")
|
|
59
|
-
return undefined;
|
|
60
|
-
return parts.at(-2);
|
|
61
|
-
}
|
|
62
|
-
function skillIds(skills) {
|
|
63
|
-
const ids = [];
|
|
64
|
-
const seen = new Set();
|
|
65
|
-
for (const skill of skills) {
|
|
66
|
-
const name = skillNameFromAgentSkill(skill);
|
|
67
|
-
if (!name || seen.has(name))
|
|
68
|
-
continue;
|
|
69
|
-
seen.add(name);
|
|
70
|
-
ids.push(name);
|
|
71
|
-
}
|
|
72
|
-
return [...ids].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
73
|
-
}
|
|
74
|
-
function sameIdList(left, right) {
|
|
75
|
-
if (left.length !== right.length)
|
|
76
|
-
return false;
|
|
77
|
-
for (let i = 0; i < left.length; i++)
|
|
78
|
-
if (left[i] !== right[i])
|
|
79
|
-
return false;
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
44
|
/**
|
|
83
45
|
* Shared system-guidance line: name advertised dynamic-catalog tools and prefer
|
|
84
46
|
* them over Grep/Shell when skills or project rules apply. Concrete skill ids
|
|
85
|
-
* stay out of the frozen baseline (they live in
|
|
86
|
-
* and in
|
|
47
|
+
* stay out of the frozen baseline (they live in the host system prompt / skill
|
|
48
|
+
* tool, and in host `<system-update>` when the catalog changes — same split as
|
|
87
49
|
* OpenCode's SkillGuidance / SkillInstructions).
|
|
88
50
|
*/
|
|
89
51
|
export function buildDynamicCatalogRoutingInstruction(options) {
|
|
@@ -108,121 +70,9 @@ export function buildDynamicCatalogRoutingInstruction(options) {
|
|
|
108
70
|
// Mirror OpenCode 1 SystemPrompt.skills / OC2 SkillInstructions.render.
|
|
109
71
|
lines.push("- Skills provide specialized instructions and workflows for specific tasks. " +
|
|
110
72
|
"Use the `skill` tool to load a skill when a task matches its description " +
|
|
111
|
-
"(
|
|
73
|
+
"(the host system prompt and `skill` tool carry names and descriptions). " +
|
|
112
74
|
"A skill that is already present in the conversation as a `<skill_content>` block " +
|
|
113
75
|
"does not need to be invoked again.");
|
|
114
76
|
}
|
|
115
77
|
return lines.join("\n");
|
|
116
78
|
}
|
|
117
|
-
function skillEntriesXml(skills, argKey) {
|
|
118
|
-
const lines = ["<available_skills>"];
|
|
119
|
-
for (const skill of skills) {
|
|
120
|
-
const name = skillNameFromAgentSkill(skill);
|
|
121
|
-
if (!name)
|
|
122
|
-
continue;
|
|
123
|
-
const description = typeof skill.description === "string" ? skill.description : "";
|
|
124
|
-
lines.push(" <skill>");
|
|
125
|
-
if (argKey === "id")
|
|
126
|
-
lines.push(` <id>${name}</id>`);
|
|
127
|
-
lines.push(` <name>${name}</name>`);
|
|
128
|
-
if (description)
|
|
129
|
-
lines.push(` <description>${description}</description>`);
|
|
130
|
-
lines.push(" </skill>");
|
|
131
|
-
}
|
|
132
|
-
lines.push("</available_skills>");
|
|
133
|
-
return lines;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* OpenCode-shaped Mid-Conversation skill update.
|
|
137
|
-
*
|
|
138
|
-
* Pure: compare `previousSkillIds` (null = not yet admitted this conversation)
|
|
139
|
-
* to the current catalog. First admission returns undefined — baseline
|
|
140
|
-
* RequestContext + routing instruction already carry the list. Later turns
|
|
141
|
-
* only speak when the id set changes (agent switch / discovery growth), matching
|
|
142
|
-
* `SkillGuidance.update` / `SkillInstructions.update`.
|
|
143
|
-
*/
|
|
144
|
-
export function buildSkillCatalogChangeReminder(options) {
|
|
145
|
-
const nextSkillIds = skillIds(options.skills);
|
|
146
|
-
if (!options.hasSkillTool) {
|
|
147
|
-
return { text: undefined, nextSkillIds: options.previousSkillIds ? [...options.previousSkillIds] : [] };
|
|
148
|
-
}
|
|
149
|
-
if (options.previousSkillIds === null) {
|
|
150
|
-
return { text: undefined, nextSkillIds };
|
|
151
|
-
}
|
|
152
|
-
if (sameIdList(options.previousSkillIds, nextSkillIds)) {
|
|
153
|
-
return { text: undefined, nextSkillIds };
|
|
154
|
-
}
|
|
155
|
-
const argKey = options.skillArgKey === "id" ? "id" : "name";
|
|
156
|
-
const callHint = argKey === "id"
|
|
157
|
-
? "Call `skill` with `{ \"id\": \"<skill-id>\" }` (host schema key is `id`)."
|
|
158
|
-
: "Call `skill` with `{ \"name\": \"<skill-id>\" }` (host schema key is `name`).";
|
|
159
|
-
const body = nextSkillIds.length === 0
|
|
160
|
-
? ["Skill guidance is no longer available. Do not use any previously listed skill."]
|
|
161
|
-
: [
|
|
162
|
-
"The available skills have changed. This list supersedes the previous available skills list.",
|
|
163
|
-
"Skills provide specialized instructions and workflows for specific tasks.",
|
|
164
|
-
"Use the skill tool to load a skill when a task matches its description.",
|
|
165
|
-
"The user may also invoke a skill directly. When that happens, its instructions appear in the conversation as a <skill_content> block, the same shape the skill tool returns. A skill that is already present this way does not need to be invoked again.",
|
|
166
|
-
`OpenCode skills are invoked with the host \`skill\` tool through GetDynamicTools / CallDynamicTool (MCP server \`${DEFAULT_TOOL_SERVER}\`, tool \`skill\`), not by Grep/Read of SKILL.md. ${callHint}`,
|
|
167
|
-
...skillEntriesXml(options.skills, argKey),
|
|
168
|
-
];
|
|
169
|
-
return {
|
|
170
|
-
text: `<system_reminder>\n${body.join("\n")}\n</system_reminder>`,
|
|
171
|
-
nextSkillIds,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Admit the current skill catalog for a conversation and return a Mid-Conversation
|
|
176
|
-
* reminder only when OpenCode would (catalog changed after baseline admission).
|
|
177
|
-
*/
|
|
178
|
-
export function takeSkillCatalogChangeReminder(conversationId, options) {
|
|
179
|
-
const previous = conversationId
|
|
180
|
-
? (admittedSkillIdsByConversation.get(conversationId) ?? null)
|
|
181
|
-
: null;
|
|
182
|
-
const { text, nextSkillIds } = buildSkillCatalogChangeReminder({
|
|
183
|
-
...options,
|
|
184
|
-
previousSkillIds: previous,
|
|
185
|
-
});
|
|
186
|
-
if (!conversationId)
|
|
187
|
-
return text;
|
|
188
|
-
if (!options.hasSkillTool && previous === null && nextSkillIds.length === 0) {
|
|
189
|
-
return text;
|
|
190
|
-
}
|
|
191
|
-
admittedSkillIdsByConversation.delete(conversationId);
|
|
192
|
-
admittedSkillIdsByConversation.set(conversationId, nextSkillIds);
|
|
193
|
-
while (admittedSkillIdsByConversation.size > 256) {
|
|
194
|
-
const oldest = admittedSkillIdsByConversation.keys().next().value;
|
|
195
|
-
if (!oldest)
|
|
196
|
-
break;
|
|
197
|
-
admittedSkillIdsByConversation.delete(oldest);
|
|
198
|
-
}
|
|
199
|
-
return text;
|
|
200
|
-
}
|
|
201
|
-
export function clearSkillCatalogAdmission(conversationId) {
|
|
202
|
-
admittedSkillIdsByConversation.delete(conversationId);
|
|
203
|
-
}
|
|
204
|
-
export function transferSkillCatalogAdmission(previousConversationId, nextConversationId) {
|
|
205
|
-
if (!previousConversationId || !nextConversationId)
|
|
206
|
-
return;
|
|
207
|
-
const prior = admittedSkillIdsByConversation.get(previousConversationId);
|
|
208
|
-
admittedSkillIdsByConversation.delete(previousConversationId);
|
|
209
|
-
admittedSkillIdsByConversation.delete(nextConversationId);
|
|
210
|
-
if (prior)
|
|
211
|
-
admittedSkillIdsByConversation.set(nextConversationId, prior);
|
|
212
|
-
}
|
|
213
|
-
export function resetSkillCatalogAdmissionsForTests() {
|
|
214
|
-
admittedSkillIdsByConversation.clear();
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* @deprecated Prefer `buildSkillCatalogChangeReminder` / `takeSkillCatalogChangeReminder`.
|
|
218
|
-
* Kept as a thin alias for callers that still expect the old name; always returns
|
|
219
|
-
* undefined for userText-based matching (OpenCode never did per-turn id nudges).
|
|
220
|
-
*/
|
|
221
|
-
export function buildSkillCatalogNudge(options) {
|
|
222
|
-
return buildSkillCatalogChangeReminder({
|
|
223
|
-
hasSkillTool: options.hasSkillTool,
|
|
224
|
-
skills: options.skills,
|
|
225
|
-
previousSkillIds: options.previousSkillIds ?? null,
|
|
226
|
-
skillArgKey: options.skillArgKey,
|
|
227
|
-
}).text;
|
|
228
|
-
}
|
package/dist/context/epoch.d.ts
CHANGED
|
@@ -7,10 +7,10 @@ export type ContextSourceSnapshot = {
|
|
|
7
7
|
};
|
|
8
8
|
export type ContextEpoch = {
|
|
9
9
|
conversationId: string;
|
|
10
|
-
/** Exact system text
|
|
10
|
+
/** Exact system text at epoch start. Empty if a legacy checkpoint has no persisted rule. */
|
|
11
11
|
baselineSystemPrompt: string;
|
|
12
12
|
baselineHash: string;
|
|
13
|
-
/** True when restart hydrated past a checkpoint without the
|
|
13
|
+
/** True when restart hydrated past a checkpoint without the last source snapshot. */
|
|
14
14
|
recovered: boolean;
|
|
15
15
|
snapshot: ContextSourceSnapshot;
|
|
16
16
|
};
|
|
@@ -27,6 +27,8 @@ export type AdmitContextEpochInput = {
|
|
|
27
27
|
* never folded into the frozen baseline.
|
|
28
28
|
*/
|
|
29
29
|
oneShotReminders?: readonly string[];
|
|
30
|
+
/** Original system rule restored with RequestContext after a restart. */
|
|
31
|
+
recoveredBaseline?: string;
|
|
30
32
|
};
|
|
31
33
|
export type AdmitContextEpochResult = {
|
|
32
34
|
action: "initialize" | "unchanged" | "updated" | "recovered";
|
|
@@ -42,8 +44,8 @@ export type AdmitContextEpochResult = {
|
|
|
42
44
|
* - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid.
|
|
43
45
|
* - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid.
|
|
44
46
|
* - Reseed same epoch: return frozen baseline bytes (not live host text).
|
|
45
|
-
* - Recovered
|
|
46
|
-
*
|
|
47
|
+
* - Recovered: restore the persisted rule and reassert live context once on the
|
|
48
|
+
* user turn. A legacy checkpoint without a rule has no original bytes.
|
|
47
49
|
*/
|
|
48
50
|
export declare function admitContextEpoch(input: AdmitContextEpochInput): AdmitContextEpochResult;
|
|
49
51
|
export declare function getContextEpoch(conversationId: string): ContextEpoch | undefined;
|
package/dist/context/epoch.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* Mirrors OpenCode research CONTEXT.md:
|
|
5
5
|
* - One Baseline System Context per conversation_id (provider-cache prefix)
|
|
6
6
|
* - Later Context Source changes → Mid-Conversation System Message on the
|
|
7
|
-
* user turn
|
|
7
|
+
* user turn; the baseline itself never changes within the epoch
|
|
8
8
|
* - Epoch ends on conversation remint (compaction / post-compaction rebase)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The baseline reaches Cursor as the frozen system-instructions rule in
|
|
11
|
+
* RequestContext (`systemInstructionsRule`), on every Run. `seedSystemPrompt`
|
|
12
|
+
* marks the Runs that start a conversation; it is not sent as a `system`
|
|
13
|
+
* message, which Cursor does not follow.
|
|
12
14
|
*/
|
|
13
15
|
import { createHash } from "node:crypto";
|
|
14
16
|
import { trace } from "../debug.js";
|
|
@@ -80,8 +82,8 @@ function combineMessages(parts) {
|
|
|
80
82
|
* - First seed: freeze baseline, return it as seedSystemPrompt; one-shots → mid.
|
|
81
83
|
* - Checkpoint turn: never returns seedSystemPrompt; source diffs + one-shots → mid.
|
|
82
84
|
* - Reseed same epoch: return frozen baseline bytes (not live host text).
|
|
83
|
-
* - Recovered
|
|
84
|
-
*
|
|
85
|
+
* - Recovered: restore the persisted rule and reassert live context once on the
|
|
86
|
+
* user turn. A legacy checkpoint without a rule has no original bytes.
|
|
85
87
|
*/
|
|
86
88
|
export function admitContextEpoch(input) {
|
|
87
89
|
const conversationId = input.conversationId;
|
|
@@ -90,17 +92,27 @@ export function admitContextEpoch(input) {
|
|
|
90
92
|
const existing = byConversationId.get(conversationId);
|
|
91
93
|
if (!existing) {
|
|
92
94
|
if (input.hasCheckpoint) {
|
|
93
|
-
// Restart / soft-evict recovery:
|
|
94
|
-
//
|
|
95
|
+
// Restart / soft-evict recovery: keep the original rule and reassert the
|
|
96
|
+
// live instructions chronologically without changing the cache prefix.
|
|
97
|
+
const baselineSystemPrompt = input.recoveredBaseline ?? "";
|
|
95
98
|
const epoch = {
|
|
96
99
|
conversationId,
|
|
97
|
-
baselineSystemPrompt
|
|
98
|
-
baselineHash: "",
|
|
100
|
+
baselineSystemPrompt,
|
|
101
|
+
baselineHash: baselineSystemPrompt ? sha(baselineSystemPrompt) : "",
|
|
99
102
|
recovered: true,
|
|
100
103
|
snapshot: nextSnap,
|
|
101
104
|
};
|
|
102
105
|
touch(epoch);
|
|
103
|
-
|
|
106
|
+
// The persisted rule is the original prefix, not necessarily the current
|
|
107
|
+
// host instructions. Reconcile live context once before recording its
|
|
108
|
+
// snapshot; otherwise a changed prompt is silently treated as admitted.
|
|
109
|
+
// Even equality with the original baseline needs an update: the last
|
|
110
|
+
// checkpoint may contain an intervening mode/instruction change.
|
|
111
|
+
const liveBaseline = buildBaseline(input);
|
|
112
|
+
const recoveryUpdate = baselineSystemPrompt && liveBaseline
|
|
113
|
+
? wrapReminder("Current host system instructions after session recovery:\n\n" + liveBaseline)
|
|
114
|
+
: "";
|
|
115
|
+
const midConversationMessage = combineMessages([recoveryUpdate, ...oneShots]);
|
|
104
116
|
trace(`context epoch: recovered conversationId=${conversationId} ` +
|
|
105
117
|
`oneShots=${oneShots.length}`);
|
|
106
118
|
return { action: "recovered", midConversationMessage, epoch };
|
package/dist/context/frozen.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
1
|
+
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, withSystemInstructions, } from "./build.js";
|
|
2
2
|
import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js";
|
|
3
3
|
import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js";
|
|
4
|
-
import { clearSkillCatalogAdmission, resetSkillCatalogAdmissionsForTests, transferSkillCatalogAdmission, } from "./dynamic-catalog.js";
|
|
5
4
|
import { trace } from "../debug.js";
|
|
6
5
|
import { encodeMessage } from "../protocol/messages.js";
|
|
7
6
|
/**
|
|
@@ -11,10 +10,10 @@ import { encodeMessage } from "../protocol/messages.js";
|
|
|
11
10
|
* base plus live plugin/tool overlays. Rebuilding volatile git/layout data on
|
|
12
11
|
* every Run shifts the prompt prefix and tanks prompt-cache hits.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
* each Run, then epoch-held (equal ids keep frozen bytes; new ids
|
|
16
|
-
* overlaid on that base. If the encoded overlay bytes did not
|
|
17
|
-
* prior materialized object is reused.
|
|
13
|
+
* Host-advertised subagents, plugin metadata, and tool/MCP capabilities are
|
|
14
|
+
* rediscovered each Run, then epoch-held (equal ids keep frozen bytes; new ids
|
|
15
|
+
* append) and overlaid on that base. If the encoded overlay bytes did not
|
|
16
|
+
* change, the exact prior materialized object is reused.
|
|
18
17
|
*/
|
|
19
18
|
const byConversationId = new Map();
|
|
20
19
|
const materializedByConversationId = new Map();
|
|
@@ -45,7 +44,6 @@ function remember(conversationId, context) {
|
|
|
45
44
|
byConversationId.delete(oldest);
|
|
46
45
|
materializedByConversationId.delete(oldest);
|
|
47
46
|
clearOverlayHold(oldest);
|
|
48
|
-
clearSkillCatalogAdmission(oldest);
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
/** Frozen stable RequestContext base for this conversation, if any. */
|
|
@@ -69,7 +67,6 @@ export function clearFrozenRequestContext(conversationId) {
|
|
|
69
67
|
byConversationId.delete(conversationId);
|
|
70
68
|
materializedByConversationId.delete(conversationId);
|
|
71
69
|
clearOverlayHold(conversationId);
|
|
72
|
-
clearSkillCatalogAdmission(conversationId);
|
|
73
70
|
clearContextEpoch(conversationId);
|
|
74
71
|
}
|
|
75
72
|
/**
|
|
@@ -87,12 +84,11 @@ export function transferFrozenRequestContext(previousConversationId, nextConvers
|
|
|
87
84
|
const base = byConversationId.get(previousConversationId);
|
|
88
85
|
const materialized = materializedByConversationId.get(previousConversationId);
|
|
89
86
|
// System Context epoch does not transfer — compaction starts a fresh baseline.
|
|
90
|
-
// Overlay hold does transfer: same workspace, same advertised
|
|
87
|
+
// Overlay hold does transfer: same workspace, same advertised agent/plugin
|
|
91
88
|
// bytes, so the comparison seed can still match. clearFrozenRequestContext
|
|
92
89
|
// also drops epoch state for each id.
|
|
93
90
|
endContextEpoch(previousConversationId, nextConversationId);
|
|
94
91
|
transferOverlayHold(previousConversationId, nextConversationId);
|
|
95
|
-
transferSkillCatalogAdmission(previousConversationId, nextConversationId);
|
|
96
92
|
byConversationId.delete(previousConversationId);
|
|
97
93
|
materializedByConversationId.delete(previousConversationId);
|
|
98
94
|
byConversationId.delete(nextConversationId);
|
|
@@ -114,7 +110,6 @@ export function resetFrozenRequestContextsForTests() {
|
|
|
114
110
|
materializedByConversationId.clear();
|
|
115
111
|
buildsByConversationId.clear();
|
|
116
112
|
resetOverlayHoldsForTests();
|
|
117
|
-
resetSkillCatalogAdmissionsForTests();
|
|
118
113
|
resetContextEpochsForTests();
|
|
119
114
|
}
|
|
120
115
|
function sameBytes(a, b) {
|
|
@@ -125,6 +120,23 @@ function sameBytes(a, b) {
|
|
|
125
120
|
return false;
|
|
126
121
|
return true;
|
|
127
122
|
}
|
|
123
|
+
function advertisedMetaToolCount(context) {
|
|
124
|
+
const meta = context.mcp_meta_tool_options;
|
|
125
|
+
if (!meta || typeof meta !== "object")
|
|
126
|
+
return 0;
|
|
127
|
+
const descriptors = meta.mcp_descriptors;
|
|
128
|
+
if (!Array.isArray(descriptors))
|
|
129
|
+
return 0;
|
|
130
|
+
let count = 0;
|
|
131
|
+
for (const descriptor of descriptors) {
|
|
132
|
+
if (!descriptor || typeof descriptor !== "object")
|
|
133
|
+
continue;
|
|
134
|
+
const tools = descriptor.tools;
|
|
135
|
+
if (Array.isArray(tools))
|
|
136
|
+
count += tools.length;
|
|
137
|
+
}
|
|
138
|
+
return count;
|
|
139
|
+
}
|
|
128
140
|
function rememberMaterialized(conversationId, context) {
|
|
129
141
|
const bytes = encodeMessage("RequestContext", context);
|
|
130
142
|
const previous = materializedByConversationId.get(conversationId);
|
|
@@ -148,12 +160,23 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
|
|
|
148
160
|
if (opts?.refresh && conversationId)
|
|
149
161
|
clearOverlayHold(conversationId);
|
|
150
162
|
if (!opts?.refresh && conversationId) {
|
|
151
|
-
|
|
163
|
+
let base = getFrozenRequestContext(conversationId);
|
|
152
164
|
if (base) {
|
|
165
|
+
// The system-instructions rule is frozen with the base. A new epoch
|
|
166
|
+
// baseline (compaction rebase, binding reset) replaces it; a recovered
|
|
167
|
+
// epoch only fills a base persisted without one.
|
|
168
|
+
const instructed = withSystemInstructions(base, input.systemInstructions);
|
|
169
|
+
if (instructed !== base) {
|
|
170
|
+
setFrozenRequestContext(conversationId, instructed);
|
|
171
|
+
base = getFrozenRequestContext(conversationId);
|
|
172
|
+
trace(`request_context: system instructions frozen conversationId=${conversationId} ` +
|
|
173
|
+
`len=${input.systemInstructions?.text.length ?? 0} ` +
|
|
174
|
+
`authoritative=${input.systemInstructions?.authoritative ?? false}`);
|
|
175
|
+
}
|
|
153
176
|
const dynamic = await buildDynamicRequestContext(scoped);
|
|
154
177
|
const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
|
|
155
178
|
trace(`request_context: materialized conversationId=${conversationId} ` +
|
|
156
|
-
`tools=${
|
|
179
|
+
`tools=${advertisedMetaToolCount(materialized.context)} ` +
|
|
157
180
|
`reused=${materialized.reused}`);
|
|
158
181
|
return materialized;
|
|
159
182
|
}
|
|
@@ -187,7 +210,7 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
|
|
|
187
210
|
? rememberMaterialized(conversationId, context)
|
|
188
211
|
: { context: freezeSnapshot(structuredClone(context)), reused: false };
|
|
189
212
|
trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` +
|
|
190
|
-
`tools=${
|
|
213
|
+
`tools=${advertisedMetaToolCount(materialized.context)} ` +
|
|
191
214
|
`refresh=${!!opts?.refresh}`);
|
|
192
215
|
return { context: materialized.context, reused: false };
|
|
193
216
|
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
|
|
2
|
-
export { buildDynamicCatalogRoutingInstruction,
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, listAdvertisedMcpServers, } from "./dynamic-catalog.js";
|
|
3
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
4
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
5
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, type AdmitContextEpochInput, type AdmitContextEpochResult, type ContextEpoch, type ContextSourceSnapshot, } from "./epoch.js";
|
package/dist/context/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
|
-
export { buildDynamicCatalogRoutingInstruction,
|
|
2
|
+
export { buildDynamicCatalogRoutingInstruction, listAdvertisedMcpServers, } from "./dynamic-catalog.js";
|
|
3
3
|
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
4
4
|
export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
|
|
5
5
|
export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, } from "./epoch.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Epoch-hold live RequestContext overlay lists (
|
|
2
|
+
* Epoch-hold live RequestContext overlay lists (subagents, plugins).
|
|
3
3
|
*
|
|
4
4
|
* Same policy as the tool catalog: first nonempty freeze is UTF-16 by id,
|
|
5
5
|
* equal ids keep frozen bytes (including content), new ids append at the
|
|
@@ -7,12 +7,6 @@
|
|
|
7
7
|
* Run; this only canonicalizes what is advertised.
|
|
8
8
|
*/
|
|
9
9
|
export declare const MAX_OVERLAY_HOLDS = 256;
|
|
10
|
-
export type OverlaySkill = {
|
|
11
|
-
id: string;
|
|
12
|
-
full_path: string;
|
|
13
|
-
content: string;
|
|
14
|
-
description: string;
|
|
15
|
-
};
|
|
16
10
|
export type OverlaySubagent = {
|
|
17
11
|
full_path: string;
|
|
18
12
|
name: string;
|
|
@@ -24,26 +18,11 @@ export type OverlayPlugin = {
|
|
|
24
18
|
line: string;
|
|
25
19
|
};
|
|
26
20
|
export type OverlayHold = {
|
|
27
|
-
skills: OverlaySkill[];
|
|
28
|
-
subagents: OverlaySubagent[];
|
|
29
|
-
plugins: OverlayPlugin[];
|
|
30
|
-
};
|
|
31
|
-
export type OverlayWire = {
|
|
32
|
-
skills: Array<{
|
|
33
|
-
full_path: string;
|
|
34
|
-
content: string;
|
|
35
|
-
description: string;
|
|
36
|
-
}>;
|
|
37
21
|
subagents: OverlaySubagent[];
|
|
38
22
|
plugins: OverlayPlugin[];
|
|
39
23
|
};
|
|
40
24
|
/** Merge live discovery into the conversation overlay epoch. */
|
|
41
|
-
export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold):
|
|
42
|
-
/**
|
|
43
|
-
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
44
|
-
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
45
|
-
*/
|
|
46
|
-
export declare function getHeldOverlaySkills(conversationId: string): OverlaySkill[];
|
|
25
|
+
export declare function holdCapabilityOverlay(conversationId: string, live: OverlayHold): OverlayHold;
|
|
47
26
|
export declare function clearOverlayHold(conversationId: string): void;
|
|
48
27
|
export declare function transferOverlayHold(previousConversationId: string, nextConversationId: string): void;
|
|
49
28
|
export declare function resetOverlayHoldsForTests(): void;
|
package/dist/context/overlay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Epoch-hold live RequestContext overlay lists (
|
|
2
|
+
* Epoch-hold live RequestContext overlay lists (subagents, plugins).
|
|
3
3
|
*
|
|
4
4
|
* Same policy as the tool catalog: first nonempty freeze is UTF-16 by id,
|
|
5
5
|
* equal ids keep frozen bytes (including content), new ids append at the
|
|
@@ -29,7 +29,6 @@ function holdList(cached, incoming, idOf) {
|
|
|
29
29
|
}
|
|
30
30
|
function remember(conversationId, hold) {
|
|
31
31
|
const stored = {
|
|
32
|
-
skills: structuredClone(hold.skills),
|
|
33
32
|
subagents: structuredClone(hold.subagents),
|
|
34
33
|
plugins: structuredClone(hold.plugins),
|
|
35
34
|
};
|
|
@@ -43,47 +42,24 @@ function remember(conversationId, hold) {
|
|
|
43
42
|
}
|
|
44
43
|
return stored;
|
|
45
44
|
}
|
|
46
|
-
function skillId(skill) {
|
|
47
|
-
return skill.id || skill.full_path;
|
|
48
|
-
}
|
|
49
|
-
function toWire(hold) {
|
|
50
|
-
return {
|
|
51
|
-
skills: hold.skills.map(({ full_path, content, description }) => ({
|
|
52
|
-
full_path,
|
|
53
|
-
content,
|
|
54
|
-
description,
|
|
55
|
-
})),
|
|
56
|
-
subagents: hold.subagents,
|
|
57
|
-
plugins: hold.plugins,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
45
|
/** Merge live discovery into the conversation overlay epoch. */
|
|
61
46
|
export function holdCapabilityOverlay(conversationId, live) {
|
|
62
47
|
if (!conversationId) {
|
|
63
|
-
return
|
|
64
|
-
skills: inFixedOrder(live.skills, skillId),
|
|
48
|
+
return {
|
|
65
49
|
subagents: inFixedOrder(live.subagents, (agent) => agent.name),
|
|
66
50
|
plugins: inFixedOrder(live.plugins, (plugin) => plugin.id),
|
|
67
|
-
}
|
|
51
|
+
};
|
|
68
52
|
}
|
|
69
53
|
const cached = byConversationId.get(conversationId);
|
|
70
54
|
const next = {
|
|
71
|
-
skills: holdList(cached?.skills, live.skills, skillId),
|
|
72
55
|
subagents: holdList(cached?.subagents, live.subagents, (agent) => agent.name),
|
|
73
56
|
plugins: holdList(cached?.plugins, live.plugins, (plugin) => plugin.id),
|
|
74
57
|
};
|
|
75
|
-
if (next.
|
|
58
|
+
if (next.subagents.length === 0 && next.plugins.length === 0) {
|
|
76
59
|
byConversationId.delete(conversationId);
|
|
77
|
-
return
|
|
60
|
+
return next;
|
|
78
61
|
}
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Epoch-held skills with their OpenCode ids. The wire shape drops `id`
|
|
83
|
-
* (`AgentSkill` has no name field), but the host `skill` tool needs it.
|
|
84
|
-
*/
|
|
85
|
-
export function getHeldOverlaySkills(conversationId) {
|
|
86
|
-
return structuredClone(byConversationId.get(conversationId)?.skills ?? []);
|
|
62
|
+
return remember(conversationId, next);
|
|
87
63
|
}
|
|
88
64
|
export function clearOverlayHold(conversationId) {
|
|
89
65
|
byConversationId.delete(conversationId);
|
package/dist/context/paths.d.ts
CHANGED
|
@@ -21,8 +21,12 @@ export declare function setHostCacheDirOverride(dir: string | undefined): void;
|
|
|
21
21
|
export declare function getHostCacheDirOverride(): string | undefined;
|
|
22
22
|
/** Resolve the native OpenCode cache root when no host bridge is installed. */
|
|
23
23
|
export declare function resolveHostCacheDir(env?: HostPathEnv): string;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
26
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
27
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
28
|
+
*/
|
|
29
|
+
export declare function opencodeGlobalConfigDir(env?: HostPathEnv): string;
|
|
26
30
|
/**
|
|
27
31
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
28
32
|
*
|
|
@@ -57,7 +61,7 @@ export declare function hostGlobalDataDir(env?: HostPathEnv): string;
|
|
|
57
61
|
*/
|
|
58
62
|
export declare function hostPlansDir(_workspaceRoot?: string): string;
|
|
59
63
|
/**
|
|
60
|
-
* Cursor-compatible path slug (`/
|
|
64
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
61
65
|
* Used for per-workspace metadata under the host cache.
|
|
62
66
|
*/
|
|
63
67
|
export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
@@ -72,4 +76,3 @@ export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
|
72
76
|
export declare function opencodeProjectDir(workspaceRoot: string): string;
|
|
73
77
|
/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */
|
|
74
78
|
export declare function ensureOpencodeProjectDir(workspaceRoot: string): string;
|
|
75
|
-
export declare function resolveHomeRelative(p: string): string;
|