@codemem/opencode-plugin 0.44.2 → 0.45.0-rc.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/.opencode/lib/delegation-context.js +202 -0
- package/.opencode/lib/host-contract.js +90 -0
- package/.opencode/lib/opencode-v2-adapter.js +739 -0
- package/.opencode/lib/runtime.js +5381 -0
- package/.opencode/package.json +1 -1
- package/.opencode/plugins/codemem.js +141 -4677
- package/README.md +37 -3
- package/index.d.ts +19 -0
- package/index.js +11 -2
- package/package.json +16 -5
- package/src/opencode-v2-contract-fixture.ts +367 -0
- package/v2-contract-fixture/index.ts +1 -0
- package/v2-contract-fixture/package.json +8 -0
- package/.opencode/plugins/runtime.js +0 -1
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import {
|
|
2
|
+
V2_ADAPTER_DIAGNOSTICS,
|
|
3
|
+
createRuntimeHost,
|
|
4
|
+
createRuntimeLocation,
|
|
5
|
+
} from "./host-contract.js";
|
|
6
|
+
import { createCodememRuntime } from "./runtime.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_EVENT_TASK_TIMEOUT_MS = 250;
|
|
9
|
+
const FAILED_TOOL_ERROR = Object.freeze({
|
|
10
|
+
name: "CodememToolCaptureError",
|
|
11
|
+
message: "OpenCode reported a failed tool without error details",
|
|
12
|
+
});
|
|
13
|
+
const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
|
|
14
|
+
const CODEMEM_RECALL_METADATA_VERSION = 1;
|
|
15
|
+
const CODEMEM_V2_PART_METADATA_KEY = "codememPart";
|
|
16
|
+
|
|
17
|
+
const asRecord = (value) =>
|
|
18
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
19
|
+
|
|
20
|
+
const hasV2CodememMarker = (part) => {
|
|
21
|
+
const candidate = asRecord(part);
|
|
22
|
+
const metadata = asRecord(candidate.metadata);
|
|
23
|
+
const marker = asRecord(metadata[CODEMEM_V2_PART_METADATA_KEY]);
|
|
24
|
+
const markerID = String(marker.id || "");
|
|
25
|
+
return candidate.type === "text"
|
|
26
|
+
&& marker.v === CODEMEM_RECALL_METADATA_VERSION
|
|
27
|
+
&& marker.synthetic === true
|
|
28
|
+
&& markerID.startsWith(CODEMEM_CONTEXT_PART_ID_PREFIX);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isV2CodememTextPart = (part, messageID = null) => {
|
|
32
|
+
if (!hasV2CodememMarker(part)) return false;
|
|
33
|
+
const marker = asRecord(asRecord(part).metadata)[CODEMEM_V2_PART_METADATA_KEY];
|
|
34
|
+
return !messageID || marker.id === `${CODEMEM_CONTEXT_PART_ID_PREFIX}${messageID}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const translateV2MessagePart = (part, { messageID, sessionID }) => {
|
|
38
|
+
const candidate = asRecord(part);
|
|
39
|
+
if (messageID && hasV2CodememMarker(candidate) && !isV2CodememTextPart(candidate, messageID)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!isV2CodememTextPart(candidate, messageID)) {
|
|
43
|
+
// Keep null explicit: V2 must never synthesize identity for an unidentified message.
|
|
44
|
+
return { ...candidate, messageID, sessionID };
|
|
45
|
+
}
|
|
46
|
+
const metadata = asRecord(candidate.metadata);
|
|
47
|
+
const marker = asRecord(metadata[CODEMEM_V2_PART_METADATA_KEY]);
|
|
48
|
+
const canonicalMetadata = { ...metadata };
|
|
49
|
+
delete canonicalMetadata[CODEMEM_V2_PART_METADATA_KEY];
|
|
50
|
+
return {
|
|
51
|
+
...candidate,
|
|
52
|
+
id: marker.id,
|
|
53
|
+
messageID,
|
|
54
|
+
sessionID,
|
|
55
|
+
synthetic: true,
|
|
56
|
+
metadata: canonicalMetadata,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const translateV2Messages = (messages, sessionID) => {
|
|
61
|
+
if (!Array.isArray(messages)) return [];
|
|
62
|
+
return messages.map((message) => {
|
|
63
|
+
const candidate = asRecord(message);
|
|
64
|
+
const messageID = typeof candidate.id === "string" && candidate.id.trim() ? candidate.id : null;
|
|
65
|
+
const content = Array.isArray(candidate.content) ? candidate.content : [];
|
|
66
|
+
return {
|
|
67
|
+
info: {
|
|
68
|
+
...(messageID ? { id: messageID } : {}),
|
|
69
|
+
role: candidate.role,
|
|
70
|
+
sessionID,
|
|
71
|
+
},
|
|
72
|
+
parts: content
|
|
73
|
+
.map((part) => translateV2MessagePart(part, { messageID, sessionID }))
|
|
74
|
+
.filter(Boolean),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const isCanonicalCodememTextPart = (part) =>
|
|
80
|
+
part?.type === "text"
|
|
81
|
+
&& part?.synthetic === true
|
|
82
|
+
&& String(part?.id || "").startsWith(CODEMEM_CONTEXT_PART_ID_PREFIX);
|
|
83
|
+
|
|
84
|
+
const toV2CodememTextPart = (part) => {
|
|
85
|
+
const metadata = asRecord(part.metadata);
|
|
86
|
+
return {
|
|
87
|
+
type: "text",
|
|
88
|
+
text: part.text,
|
|
89
|
+
metadata: {
|
|
90
|
+
...metadata,
|
|
91
|
+
[CODEMEM_V2_PART_METADATA_KEY]: {
|
|
92
|
+
v: CODEMEM_RECALL_METADATA_VERSION,
|
|
93
|
+
synthetic: true,
|
|
94
|
+
id: part.id,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const copyInjectedContextToV2Messages = (messages, canonicalMessages) =>
|
|
101
|
+
messages.map((original, index) => {
|
|
102
|
+
const message = asRecord(original);
|
|
103
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
104
|
+
const canonicalMessage = canonicalMessages[index];
|
|
105
|
+
const hasMessageIdentity = Boolean(canonicalMessage?.info?.id);
|
|
106
|
+
const injected = canonicalMessage?.parts
|
|
107
|
+
?.filter(isCanonicalCodememTextPart)
|
|
108
|
+
.map(toV2CodememTextPart) || [];
|
|
109
|
+
const retained = content.flatMap((part) => {
|
|
110
|
+
if (!hasV2CodememMarker(part)) return [part];
|
|
111
|
+
const markerID = part.metadata[CODEMEM_V2_PART_METADATA_KEY].id;
|
|
112
|
+
const replacementIndex = injected.findIndex(
|
|
113
|
+
(candidate) => candidate.metadata[CODEMEM_V2_PART_METADATA_KEY].id === markerID,
|
|
114
|
+
);
|
|
115
|
+
if (replacementIndex < 0) return hasMessageIdentity ? [] : [part];
|
|
116
|
+
return injected.splice(replacementIndex, 1);
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
...message,
|
|
120
|
+
content: [...retained, ...injected],
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const copyLatestInjectedContextToV2System = (system, canonicalMessages) => {
|
|
125
|
+
for (let index = canonicalMessages.length - 1; index >= 0; index -= 1) {
|
|
126
|
+
const message = canonicalMessages[index];
|
|
127
|
+
if (message?.info?.role !== "user") continue;
|
|
128
|
+
const injected = message.parts?.find(isCanonicalCodememTextPart);
|
|
129
|
+
if (!injected) return null;
|
|
130
|
+
return [
|
|
131
|
+
...system.filter((part) => !hasV2CodememMarker(part)),
|
|
132
|
+
toV2CodememTextPart(injected),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const transformV2Context = async (runtime, input, { isActive = () => true } = {}) => {
|
|
139
|
+
const messages = Array.isArray(input?.messages) ? input.messages : [];
|
|
140
|
+
const canonicalMessages = translateV2Messages(messages, input?.sessionID || null);
|
|
141
|
+
const result = await runtime.transformMessages(
|
|
142
|
+
{ sessionID: input?.sessionID || null },
|
|
143
|
+
{ messages: canonicalMessages },
|
|
144
|
+
{
|
|
145
|
+
deferDeliveryConfirmation: true,
|
|
146
|
+
enableSystemSurface: true,
|
|
147
|
+
pruneAbsentCacheEntries: false,
|
|
148
|
+
requireLatestUserMessageID: true,
|
|
149
|
+
cacheSuccessfulEmpty: true,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
if (!isActive()) {
|
|
153
|
+
await result?.completeDelivery?.("failed");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
if (result?.surface === "system") {
|
|
158
|
+
const system = Array.isArray(input?.system) ? input.system : [];
|
|
159
|
+
const transformed = copyLatestInjectedContextToV2System(system, canonicalMessages);
|
|
160
|
+
if (transformed && !Array.isArray(input?.system)) {
|
|
161
|
+
throw new TypeError("OpenCode V2 context input omitted the mutable system array");
|
|
162
|
+
}
|
|
163
|
+
if (transformed) {
|
|
164
|
+
input.system.splice(0, input.system.length, ...transformed);
|
|
165
|
+
}
|
|
166
|
+
await result?.completeDelivery?.("handed_off");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(input?.messages)) {
|
|
170
|
+
if (result?.applied) {
|
|
171
|
+
throw new TypeError("OpenCode V2 context input omitted the mutable messages array");
|
|
172
|
+
}
|
|
173
|
+
await result?.completeDelivery?.("handed_off");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const transformed = copyInjectedContextToV2Messages(messages, canonicalMessages);
|
|
177
|
+
input.messages.splice(0, input.messages.length, ...transformed);
|
|
178
|
+
await result?.completeDelivery?.("handed_off");
|
|
179
|
+
} catch (error) {
|
|
180
|
+
await result?.completeDelivery?.("failed");
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const createContextScheduler = () => {
|
|
186
|
+
const tails = new Map();
|
|
187
|
+
const schedule = async (key, run) => {
|
|
188
|
+
const previous = tails.get(key) || Promise.resolve();
|
|
189
|
+
// Serialize retries of one turn without blocking independent turns or sessions.
|
|
190
|
+
const task = previous.catch(() => {}).then(run);
|
|
191
|
+
tails.set(key, task);
|
|
192
|
+
try {
|
|
193
|
+
await task;
|
|
194
|
+
} finally {
|
|
195
|
+
if (tails.get(key) === task) tails.delete(key);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
schedule,
|
|
200
|
+
waitForIdle: () => Promise.allSettled([...tails.values()]),
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const contextScheduleKey = (input) => {
|
|
205
|
+
const messages = Array.isArray(input?.messages) ? input.messages : [];
|
|
206
|
+
const latestUser = messages.findLast((message) => message?.role === "user");
|
|
207
|
+
const messageID = typeof latestUser?.id === "string" ? latestUser.id : "missing";
|
|
208
|
+
return `${input?.sessionID || "unknown"}:${messageID}`;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const canonicalEvent = (event, overrides) => ({
|
|
212
|
+
type: overrides.type,
|
|
213
|
+
sessionID: overrides.sessionID,
|
|
214
|
+
messageInfo: overrides.messageInfo || null,
|
|
215
|
+
part: overrides.part || null,
|
|
216
|
+
usage: overrides.usage || null,
|
|
217
|
+
raw: event,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const tokenCount = (value) =>
|
|
221
|
+
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
222
|
+
|
|
223
|
+
const addTokens = (left = {}, right = {}) => ({
|
|
224
|
+
input: tokenCount(left.input) + tokenCount(right.input),
|
|
225
|
+
output: tokenCount(left.output) + tokenCount(right.output),
|
|
226
|
+
cache: {
|
|
227
|
+
read: tokenCount(left.cache?.read) + tokenCount(right.cache?.read),
|
|
228
|
+
write: tokenCount(left.cache?.write) + tokenCount(right.cache?.write),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const usageKey = (sessionID, messageID) => `${sessionID}:${messageID}`;
|
|
233
|
+
|
|
234
|
+
export const createV2EventTranslator = () => {
|
|
235
|
+
const usageByMessage = new Map();
|
|
236
|
+
const textByMessage = new Map();
|
|
237
|
+
|
|
238
|
+
const clearSession = (sessionID) => {
|
|
239
|
+
for (const key of usageByMessage.keys()) {
|
|
240
|
+
if (key.startsWith(`${sessionID}:`)) usageByMessage.delete(key);
|
|
241
|
+
}
|
|
242
|
+
for (const key of textByMessage.keys()) {
|
|
243
|
+
if (key.startsWith(`${sessionID}:`)) textByMessage.delete(key);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const translateInbox = (event, data, sessionID) => {
|
|
248
|
+
if (data.item?.type !== "user") return [];
|
|
249
|
+
const messageID = data.inboxID;
|
|
250
|
+
const text = data.item.payload?.text;
|
|
251
|
+
if (typeof messageID !== "string" || typeof text !== "string") return [];
|
|
252
|
+
return [
|
|
253
|
+
canonicalEvent(event, {
|
|
254
|
+
type: "message.updated",
|
|
255
|
+
sessionID,
|
|
256
|
+
messageInfo: { id: messageID, role: "user", sessionID },
|
|
257
|
+
}),
|
|
258
|
+
canonicalEvent(event, {
|
|
259
|
+
type: "message.part.updated",
|
|
260
|
+
sessionID,
|
|
261
|
+
part: { id: event.id, messageID, sessionID, type: "text", text },
|
|
262
|
+
}),
|
|
263
|
+
];
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const translateText = (event, data, sessionID) => {
|
|
267
|
+
const messageID = data.assistantMessageID;
|
|
268
|
+
if (!sessionID || typeof messageID !== "string" || typeof data.text !== "string") return [];
|
|
269
|
+
const key = usageKey(sessionID, messageID);
|
|
270
|
+
const text = `${textByMessage.get(key) || ""}${data.text}`;
|
|
271
|
+
textByMessage.set(key, text);
|
|
272
|
+
return [
|
|
273
|
+
canonicalEvent(event, {
|
|
274
|
+
type: "message.part.updated",
|
|
275
|
+
sessionID,
|
|
276
|
+
part: { id: event.id, messageID, sessionID, type: "text", text },
|
|
277
|
+
}),
|
|
278
|
+
];
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const translateStep = (event, data, sessionID) => {
|
|
282
|
+
const messageID = data.assistantMessageID;
|
|
283
|
+
if (!sessionID || typeof messageID !== "string") return [];
|
|
284
|
+
const key = usageKey(sessionID, messageID);
|
|
285
|
+
const tokens = addTokens(usageByMessage.get(key), asRecord(data.tokens));
|
|
286
|
+
usageByMessage.set(key, tokens);
|
|
287
|
+
if (data.finish === "tool-calls") return [];
|
|
288
|
+
usageByMessage.delete(key);
|
|
289
|
+
textByMessage.delete(key);
|
|
290
|
+
return [canonicalEvent(event, {
|
|
291
|
+
type: "message.updated",
|
|
292
|
+
sessionID,
|
|
293
|
+
messageInfo: { id: messageID, role: "assistant", sessionID, finish: true, tokens },
|
|
294
|
+
usage: tokens,
|
|
295
|
+
})];
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const translate = (event) => {
|
|
299
|
+
const data = asRecord(event?.data);
|
|
300
|
+
const sessionID = typeof data.sessionID === "string" ? data.sessionID : null;
|
|
301
|
+
if (event?.type === "session.inbox.enqueued") return translateInbox(event, data, sessionID);
|
|
302
|
+
if (event?.type === "session.text.ended") return translateText(event, data, sessionID);
|
|
303
|
+
if (event?.type === "session.step.ended") return translateStep(event, data, sessionID);
|
|
304
|
+
if (event?.type === "session.created") {
|
|
305
|
+
return [canonicalEvent(event, { type: "session.created", sessionID })];
|
|
306
|
+
}
|
|
307
|
+
if (event?.type === "session.deleted") {
|
|
308
|
+
if (sessionID) clearSession(sessionID);
|
|
309
|
+
return [canonicalEvent(event, { type: "session.deleted", sessionID })];
|
|
310
|
+
}
|
|
311
|
+
if (event?.type === "session.execution.failed") {
|
|
312
|
+
if (sessionID) clearSession(sessionID);
|
|
313
|
+
return [canonicalEvent(event, { type: "session.error", sessionID })];
|
|
314
|
+
}
|
|
315
|
+
if (
|
|
316
|
+
event?.type === "session.execution.succeeded"
|
|
317
|
+
|| event?.type === "session.execution.interrupted"
|
|
318
|
+
) {
|
|
319
|
+
if (sessionID) clearSession(sessionID);
|
|
320
|
+
return [canonicalEvent(event, { type: "session.idle", sessionID })];
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
clear: () => {
|
|
327
|
+
usageByMessage.clear();
|
|
328
|
+
textByMessage.clear();
|
|
329
|
+
},
|
|
330
|
+
translate,
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const translateV2Event = (event) => createV2EventTranslator().translate(event);
|
|
335
|
+
|
|
336
|
+
export const translateV2ToolResult = (input) => {
|
|
337
|
+
const failed = input?.status === "error";
|
|
338
|
+
const result = asRecord(input?.result);
|
|
339
|
+
let output = null;
|
|
340
|
+
if (!failed) output = result.output ?? result.content ?? input?.result ?? null;
|
|
341
|
+
return {
|
|
342
|
+
input: {
|
|
343
|
+
id: typeof input?.id === "string" ? input.id : null,
|
|
344
|
+
sessionID: typeof input?.sessionID === "string" ? input.sessionID : null,
|
|
345
|
+
tool: typeof input?.tool === "string" ? input.tool : "unknown",
|
|
346
|
+
args: asRecord(input?.input),
|
|
347
|
+
},
|
|
348
|
+
output: {
|
|
349
|
+
output,
|
|
350
|
+
error: failed ? input.error || FAILED_TOOL_ERROR : null,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const createV2Tool = (name, definition, { isActive = () => true } = {}) => {
|
|
356
|
+
const properties = {};
|
|
357
|
+
const required = [];
|
|
358
|
+
for (const [argumentName, argument] of Object.entries(definition.args)) {
|
|
359
|
+
if (argument.type !== "number") {
|
|
360
|
+
throw new Error(`Unsupported OpenCode 2 tool argument: ${argumentName}:${argument.type}`);
|
|
361
|
+
}
|
|
362
|
+
properties[argumentName] = { type: "number" };
|
|
363
|
+
if (!argument.optional) required.push(argumentName);
|
|
364
|
+
}
|
|
365
|
+
const input = {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties,
|
|
368
|
+
additionalProperties: false,
|
|
369
|
+
...(required.length > 0 ? { required } : {}),
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
name,
|
|
373
|
+
description: definition.description,
|
|
374
|
+
input,
|
|
375
|
+
options: { codemode: false },
|
|
376
|
+
execute: async (args) => {
|
|
377
|
+
if (!isActive()) throw new Error("Codemem tool is unavailable after adapter cleanup");
|
|
378
|
+
return { content: await definition.execute(asRecord(args)) };
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const defaultWaitForEventTask = (eventTask, timeoutMs) =>
|
|
384
|
+
new Promise((resolve) => {
|
|
385
|
+
let settled = false;
|
|
386
|
+
const finish = (completed) => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
resolve(completed);
|
|
391
|
+
};
|
|
392
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
393
|
+
eventTask.then(() => finish(true), () => finish(true));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const waitForRegistrationDisposal = async (registrations, waitForTask, timeoutMs) => {
|
|
397
|
+
const outcomes = await Promise.all([...registrations].reverse().map(async (registration) => {
|
|
398
|
+
let disposalError;
|
|
399
|
+
const disposalTask = Promise.resolve().then(() => registration.dispose()).catch((error) => {
|
|
400
|
+
disposalError = error;
|
|
401
|
+
});
|
|
402
|
+
try {
|
|
403
|
+
const completed = await waitForTask(disposalTask, timeoutMs);
|
|
404
|
+
return { completed, error: disposalError };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return { completed: false, error };
|
|
407
|
+
}
|
|
408
|
+
}));
|
|
409
|
+
return {
|
|
410
|
+
completed: outcomes.every((outcome) => outcome.completed),
|
|
411
|
+
error: outcomes.find((outcome) => outcome.error)?.error,
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const waitForRuntimeDisposal = (runtime, waitForTask, timeoutMs) =>
|
|
416
|
+
waitForRegistrationDisposal(
|
|
417
|
+
[{ dispose: () => runtime.dispose() }],
|
|
418
|
+
waitForTask,
|
|
419
|
+
timeoutMs,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const reportDiagnosticSafely = async (runtime, code) => {
|
|
423
|
+
try {
|
|
424
|
+
await runtime.reportDiagnostic?.(code);
|
|
425
|
+
} catch {
|
|
426
|
+
// Diagnostics must never alter host behavior.
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const reportDiagnosticWithinTimeout = async (runtime, code, waitForTask, timeoutMs) => {
|
|
431
|
+
const diagnosticTask = reportDiagnosticSafely(runtime, code);
|
|
432
|
+
try {
|
|
433
|
+
await waitForTask(diagnosticTask, timeoutMs);
|
|
434
|
+
} catch {
|
|
435
|
+
// Diagnostics must never delay or alter cleanup.
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const createCaptureScheduler = () => {
|
|
440
|
+
let captureTail = Promise.resolve();
|
|
441
|
+
return (run) => {
|
|
442
|
+
const task = captureTail.then(run);
|
|
443
|
+
captureTail = task.catch(() => {});
|
|
444
|
+
return task;
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const waitForCaptureOrAbort = (task, signal) => new Promise((resolve) => {
|
|
449
|
+
let settled = false;
|
|
450
|
+
const finish = () => {
|
|
451
|
+
if (settled) return;
|
|
452
|
+
settled = true;
|
|
453
|
+
signal.removeEventListener("abort", finish);
|
|
454
|
+
resolve();
|
|
455
|
+
};
|
|
456
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
457
|
+
task.then(finish, finish);
|
|
458
|
+
if (signal.aborted) finish();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const captureRuntimeTaskWithinTimeout = async ({
|
|
462
|
+
runtime,
|
|
463
|
+
run,
|
|
464
|
+
scheduleCapture = (capture) => Promise.resolve().then(capture),
|
|
465
|
+
diagnosticCode,
|
|
466
|
+
timeoutMs,
|
|
467
|
+
waitForCaptureTask,
|
|
468
|
+
waitForDiagnosticTask,
|
|
469
|
+
}) => {
|
|
470
|
+
let failed = false;
|
|
471
|
+
const task = scheduleCapture(run).catch(() => {
|
|
472
|
+
failed = true;
|
|
473
|
+
});
|
|
474
|
+
let completed = false;
|
|
475
|
+
try {
|
|
476
|
+
completed = await waitForCaptureTask(task, timeoutMs);
|
|
477
|
+
} catch {
|
|
478
|
+
failed = true;
|
|
479
|
+
}
|
|
480
|
+
if (completed && !failed) return { completed, task };
|
|
481
|
+
await reportDiagnosticWithinTimeout(
|
|
482
|
+
runtime,
|
|
483
|
+
diagnosticCode,
|
|
484
|
+
waitForDiagnosticTask,
|
|
485
|
+
timeoutMs,
|
|
486
|
+
);
|
|
487
|
+
return { completed, task };
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const consumeEvents = async (
|
|
491
|
+
context,
|
|
492
|
+
signal,
|
|
493
|
+
runtime,
|
|
494
|
+
translator,
|
|
495
|
+
{ eventTaskTimeoutMs, scheduleCapture, waitForCaptureTask, waitForDiagnosticTask },
|
|
496
|
+
) => {
|
|
497
|
+
try {
|
|
498
|
+
for await (const event of context.event.subscribe({ signal })) {
|
|
499
|
+
if (signal.aborted) break;
|
|
500
|
+
for (const translated of translator.translate(event)) {
|
|
501
|
+
const capture = await captureRuntimeTaskWithinTimeout({
|
|
502
|
+
runtime,
|
|
503
|
+
run: () => runtime.handleEvent(translated),
|
|
504
|
+
scheduleCapture,
|
|
505
|
+
diagnosticCode: V2_ADAPTER_DIAGNOSTICS.eventCaptureFailed,
|
|
506
|
+
timeoutMs: eventTaskTimeoutMs,
|
|
507
|
+
waitForCaptureTask,
|
|
508
|
+
waitForDiagnosticTask,
|
|
509
|
+
});
|
|
510
|
+
if (!capture.completed) {
|
|
511
|
+
await waitForCaptureOrAbort(capture.task, signal);
|
|
512
|
+
if (signal.aborted) return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (!signal.aborted) {
|
|
517
|
+
await reportDiagnosticWithinTimeout(
|
|
518
|
+
runtime,
|
|
519
|
+
V2_ADAPTER_DIAGNOSTICS.eventStreamEndedUnexpectedly,
|
|
520
|
+
waitForDiagnosticTask,
|
|
521
|
+
eventTaskTimeoutMs,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (signal.aborted && error instanceof Error && error.name === "AbortError") return;
|
|
526
|
+
await reportDiagnosticWithinTimeout(
|
|
527
|
+
runtime,
|
|
528
|
+
V2_ADAPTER_DIAGNOSTICS.eventStreamFailed,
|
|
529
|
+
waitForDiagnosticTask,
|
|
530
|
+
eventTaskTimeoutMs,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const cleanupAdapter = async ({
|
|
536
|
+
abortController,
|
|
537
|
+
eventTask,
|
|
538
|
+
eventTaskTimeoutMs,
|
|
539
|
+
contextTask,
|
|
540
|
+
registrations,
|
|
541
|
+
runtime,
|
|
542
|
+
translator,
|
|
543
|
+
waitForDiagnosticTask,
|
|
544
|
+
waitForContextTask,
|
|
545
|
+
waitForEventTask,
|
|
546
|
+
waitForRegistrationTask,
|
|
547
|
+
waitForRuntimeDisposalTask,
|
|
548
|
+
}) => {
|
|
549
|
+
runtime.deactivate?.();
|
|
550
|
+
abortController.abort();
|
|
551
|
+
const registrationCleanup = await waitForRegistrationDisposal(
|
|
552
|
+
registrations,
|
|
553
|
+
waitForRegistrationTask,
|
|
554
|
+
eventTaskTimeoutMs,
|
|
555
|
+
);
|
|
556
|
+
let firstError = registrationCleanup.error;
|
|
557
|
+
if (!registrationCleanup.completed) {
|
|
558
|
+
await reportDiagnosticWithinTimeout(
|
|
559
|
+
runtime,
|
|
560
|
+
V2_ADAPTER_DIAGNOSTICS.registrationCleanupTimeout,
|
|
561
|
+
waitForDiagnosticTask,
|
|
562
|
+
eventTaskTimeoutMs,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
let contextCompleted = false;
|
|
566
|
+
try {
|
|
567
|
+
contextCompleted = await waitForContextTask(contextTask, eventTaskTimeoutMs);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
firstError ??= error;
|
|
570
|
+
}
|
|
571
|
+
if (!contextCompleted) {
|
|
572
|
+
await reportDiagnosticWithinTimeout(
|
|
573
|
+
runtime,
|
|
574
|
+
V2_ADAPTER_DIAGNOSTICS.contextCleanupTimeout,
|
|
575
|
+
waitForDiagnosticTask,
|
|
576
|
+
eventTaskTimeoutMs,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
let completed = false;
|
|
580
|
+
try {
|
|
581
|
+
completed = await waitForEventTask(eventTask, eventTaskTimeoutMs);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
firstError ??= error;
|
|
584
|
+
}
|
|
585
|
+
translator.clear();
|
|
586
|
+
if (!completed) {
|
|
587
|
+
await reportDiagnosticWithinTimeout(
|
|
588
|
+
runtime,
|
|
589
|
+
V2_ADAPTER_DIAGNOSTICS.eventStreamCleanupTimeout,
|
|
590
|
+
waitForDiagnosticTask,
|
|
591
|
+
eventTaskTimeoutMs,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const runtimeCleanup = await waitForRuntimeDisposal(
|
|
595
|
+
runtime,
|
|
596
|
+
waitForRuntimeDisposalTask,
|
|
597
|
+
eventTaskTimeoutMs,
|
|
598
|
+
);
|
|
599
|
+
firstError ??= runtimeCleanup.error;
|
|
600
|
+
if (!runtimeCleanup.completed) {
|
|
601
|
+
await reportDiagnosticWithinTimeout(
|
|
602
|
+
runtime,
|
|
603
|
+
V2_ADAPTER_DIAGNOSTICS.runtimeCleanupTimeout,
|
|
604
|
+
waitForDiagnosticTask,
|
|
605
|
+
eventTaskTimeoutMs,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (firstError) throw firstError;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
export const createOpenCodeV2Adapter = ({
|
|
612
|
+
createRuntime = createCodememRuntime,
|
|
613
|
+
eventTaskTimeoutMs = DEFAULT_EVENT_TASK_TIMEOUT_MS,
|
|
614
|
+
waitForCaptureTask = defaultWaitForEventTask,
|
|
615
|
+
waitForContextTask = defaultWaitForEventTask,
|
|
616
|
+
waitForDiagnosticTask = defaultWaitForEventTask,
|
|
617
|
+
waitForEventTask = defaultWaitForEventTask,
|
|
618
|
+
waitForRegistrationTask = defaultWaitForEventTask,
|
|
619
|
+
waitForRuntimeDisposalTask = defaultWaitForEventTask,
|
|
620
|
+
} = {}) => async (context) => {
|
|
621
|
+
const location = context.location;
|
|
622
|
+
// OpenCode 2.0.2 exposes neither app logging nor toast APIs, so V2 uses local diagnostics only.
|
|
623
|
+
const runtime = await createRuntime({
|
|
624
|
+
location: createRuntimeLocation({
|
|
625
|
+
project: { ...location.project, root: location.project.canonical },
|
|
626
|
+
directory: location.directory,
|
|
627
|
+
worktree: location.project.directory,
|
|
628
|
+
}),
|
|
629
|
+
host: createRuntimeHost({ log: async () => {}, notify: null }),
|
|
630
|
+
});
|
|
631
|
+
if (!runtime) return undefined;
|
|
632
|
+
|
|
633
|
+
const abortController = new AbortController();
|
|
634
|
+
const registrations = [];
|
|
635
|
+
const translator = createV2EventTranslator();
|
|
636
|
+
const scheduleCapture = createCaptureScheduler();
|
|
637
|
+
const scheduleContext = createContextScheduler();
|
|
638
|
+
let active = true;
|
|
639
|
+
let eventTask;
|
|
640
|
+
try {
|
|
641
|
+
registrations.push(await context.session.hook("context", async (input) => {
|
|
642
|
+
if (!active) return;
|
|
643
|
+
try {
|
|
644
|
+
await scheduleContext.schedule(contextScheduleKey(input), async () => {
|
|
645
|
+
if (active) await transformV2Context(runtime, input, { isActive: () => active });
|
|
646
|
+
});
|
|
647
|
+
} catch {
|
|
648
|
+
await reportDiagnosticWithinTimeout(
|
|
649
|
+
runtime,
|
|
650
|
+
V2_ADAPTER_DIAGNOSTICS.contextRecallFailed,
|
|
651
|
+
waitForDiagnosticTask,
|
|
652
|
+
eventTaskTimeoutMs,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}));
|
|
656
|
+
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
657
|
+
if (!active) return;
|
|
658
|
+
const translated = translateV2ToolResult(input);
|
|
659
|
+
await captureRuntimeTaskWithinTimeout({
|
|
660
|
+
runtime,
|
|
661
|
+
run: () => runtime.handleToolResult(translated.input, translated.output),
|
|
662
|
+
scheduleCapture,
|
|
663
|
+
diagnosticCode: V2_ADAPTER_DIAGNOSTICS.toolCaptureFailed,
|
|
664
|
+
timeoutMs: eventTaskTimeoutMs,
|
|
665
|
+
waitForCaptureTask,
|
|
666
|
+
waitForDiagnosticTask,
|
|
667
|
+
});
|
|
668
|
+
}));
|
|
669
|
+
registrations.push(await context.tool.transform((editor) => {
|
|
670
|
+
if (!active) return;
|
|
671
|
+
for (const [name, definition] of Object.entries(runtime.tools)) {
|
|
672
|
+
editor.add(createV2Tool(name, definition, { isActive: () => active }));
|
|
673
|
+
}
|
|
674
|
+
}));
|
|
675
|
+
eventTask = consumeEvents(context, abortController.signal, runtime, translator, {
|
|
676
|
+
eventTaskTimeoutMs,
|
|
677
|
+
scheduleCapture,
|
|
678
|
+
waitForCaptureTask,
|
|
679
|
+
waitForDiagnosticTask,
|
|
680
|
+
});
|
|
681
|
+
} catch (error) {
|
|
682
|
+
active = false;
|
|
683
|
+
runtime.deactivate?.();
|
|
684
|
+
abortController.abort();
|
|
685
|
+
translator.clear();
|
|
686
|
+
const registrationCleanup = await waitForRegistrationDisposal(
|
|
687
|
+
registrations,
|
|
688
|
+
waitForRegistrationTask,
|
|
689
|
+
eventTaskTimeoutMs,
|
|
690
|
+
);
|
|
691
|
+
if (!registrationCleanup.completed) {
|
|
692
|
+
await reportDiagnosticWithinTimeout(
|
|
693
|
+
runtime,
|
|
694
|
+
V2_ADAPTER_DIAGNOSTICS.registrationCleanupTimeout,
|
|
695
|
+
waitForDiagnosticTask,
|
|
696
|
+
eventTaskTimeoutMs,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const runtimeCleanup = await waitForRuntimeDisposal(
|
|
700
|
+
runtime,
|
|
701
|
+
waitForRuntimeDisposalTask,
|
|
702
|
+
eventTaskTimeoutMs,
|
|
703
|
+
);
|
|
704
|
+
if (!runtimeCleanup.completed) {
|
|
705
|
+
await reportDiagnosticWithinTimeout(
|
|
706
|
+
runtime,
|
|
707
|
+
V2_ADAPTER_DIAGNOSTICS.runtimeCleanupTimeout,
|
|
708
|
+
waitForDiagnosticTask,
|
|
709
|
+
eventTaskTimeoutMs,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
// Preserve the setup error that explains why activation failed.
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let cleanupTask;
|
|
717
|
+
return () => {
|
|
718
|
+
if (!cleanupTask) {
|
|
719
|
+
active = false;
|
|
720
|
+
cleanupTask = cleanupAdapter({
|
|
721
|
+
abortController,
|
|
722
|
+
contextTask: scheduleContext.waitForIdle(),
|
|
723
|
+
eventTask,
|
|
724
|
+
eventTaskTimeoutMs,
|
|
725
|
+
registrations,
|
|
726
|
+
runtime,
|
|
727
|
+
translator,
|
|
728
|
+
waitForDiagnosticTask,
|
|
729
|
+
waitForContextTask,
|
|
730
|
+
waitForEventTask,
|
|
731
|
+
waitForRegistrationTask,
|
|
732
|
+
waitForRuntimeDisposalTask,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
return cleanupTask;
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
export const setupOpenCodeV2 = createOpenCodeV2Adapter();
|