opencode-muse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +120 -2
- package/dist/catalog.d.ts +47 -0
- package/dist/catalog.js +159 -0
- package/dist/errors.d.ts +92 -0
- package/dist/errors.js +181 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +17 -0
- package/dist/model/agent-context.d.ts +10 -0
- package/dist/model/agent-context.js +35 -0
- package/dist/model/context.d.ts +26 -0
- package/dist/model/context.js +87 -0
- package/dist/model/language-model.d.ts +48 -0
- package/dist/model/language-model.js +540 -0
- package/dist/model/meta.d.ts +16 -0
- package/dist/model/meta.js +171 -0
- package/dist/model/prompt.d.ts +68 -0
- package/dist/model/prompt.js +175 -0
- package/dist/model/stream.d.ts +46 -0
- package/dist/model/stream.js +235 -0
- package/dist/msp/client.d.ts +49 -0
- package/dist/msp/client.js +229 -0
- package/dist/msp/host.d.ts +55 -0
- package/dist/msp/host.js +173 -0
- package/dist/provider.d.ts +18 -0
- package/dist/provider.js +112 -0
- package/dist/session/fingerprint.d.ts +9 -0
- package/dist/session/fingerprint.js +83 -0
- package/dist/session/manager.d.ts +40 -0
- package/dist/session/manager.js +353 -0
- package/dist/session/registry.d.ts +55 -0
- package/dist/session/registry.js +330 -0
- package/dist/session/workspace-lock.d.ts +26 -0
- package/dist/session/workspace-lock.js +288 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MspCommandRejectedError, MuseBridgeError, MuseTranscriptDivergedError, MuseUnsupportedInputError, MuseUnsupportedRequestError, MspProtocolError, MspTransportError } from "../errors.js";
|
|
3
|
+
import { MUSE_CONTEXT_HEADERS, readRequestContext } from "./context.js";
|
|
4
|
+
import { classifyMetaRequest, makeCompactionDigest, makeLocalTitle } from "./meta.js";
|
|
5
|
+
import { promptContainsCompactionFingerprint } from "../session/fingerprint.js";
|
|
6
|
+
import { extractTurnInput, validateModelOptions, } from "./prompt.js";
|
|
7
|
+
import { MuseStreamMapper } from "./stream.js";
|
|
8
|
+
const DEFAULT_VIDEO_CHUNK_BYTES = 6 * 1024 * 1024;
|
|
9
|
+
function fingerprint(method, params) {
|
|
10
|
+
return createHash("sha256").update(JSON.stringify({ method, params })).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
function sameSelection(current, next) {
|
|
13
|
+
return current !== undefined
|
|
14
|
+
&& current.providerId === next.providerId
|
|
15
|
+
&& current.modelId === next.modelId
|
|
16
|
+
&& (current.profileId ?? null) === next.profileId;
|
|
17
|
+
}
|
|
18
|
+
async function submitCommand(session, method, commandId, params) {
|
|
19
|
+
const requestParams = { ...params, commandId };
|
|
20
|
+
const requestFingerprint = fingerprint(method, requestParams);
|
|
21
|
+
await session.recordPendingCommand?.({ commandId, method, fingerprint: requestFingerprint });
|
|
22
|
+
try {
|
|
23
|
+
const result = await session.host.command(method, requestParams);
|
|
24
|
+
if (method !== "turn/start") {
|
|
25
|
+
try {
|
|
26
|
+
await session.clearPendingCommand?.(commandId);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// The command ACK proves its outcome. Keep the conservative pending
|
|
30
|
+
// fingerprint; the manager clears acknowledged transaction commands at
|
|
31
|
+
// terminal completion. Never discard a successful model turn because a
|
|
32
|
+
// registry cleanup write failed after the remote side effect.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A typed command rejection is an explicit refusal. Timeouts and transport
|
|
39
|
+
// loss leave the pending fingerprint for recovery; never retry with a new ID.
|
|
40
|
+
if (error instanceof MspCommandRejectedError)
|
|
41
|
+
await session.clearPendingCommand?.(commandId);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function mapVideoParts(session, parts, signal, chunkBytes = DEFAULT_VIDEO_CHUNK_BYTES) {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (part.type !== "video") {
|
|
49
|
+
result.push(part);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (signal?.aborted)
|
|
53
|
+
throw new DOMException("Muse request aborted before turn start", "AbortError");
|
|
54
|
+
const bytes = Buffer.from(part.base64Data, "base64");
|
|
55
|
+
if (bytes.byteLength !== part.totalBytes || bytes.byteLength === 0)
|
|
56
|
+
throw new MspProtocolError("media/upload");
|
|
57
|
+
const uploadId = session.host.mintCommandId();
|
|
58
|
+
const chunkCount = Math.ceil(bytes.byteLength / chunkBytes);
|
|
59
|
+
for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex += 1) {
|
|
60
|
+
if (signal?.aborted)
|
|
61
|
+
throw new DOMException("Muse request aborted before turn start", "AbortError");
|
|
62
|
+
const chunk = bytes.subarray(chunkIndex * chunkBytes, Math.min(bytes.byteLength, (chunkIndex + 1) * chunkBytes));
|
|
63
|
+
const commandId = session.host.mintCommandId();
|
|
64
|
+
const receipt = await submitCommand(session, "media/upload", commandId, {
|
|
65
|
+
sessionId: session.sessionId,
|
|
66
|
+
uploadId,
|
|
67
|
+
mediaType: part.mediaType,
|
|
68
|
+
chunkIndex,
|
|
69
|
+
chunkCount,
|
|
70
|
+
totalBytes: bytes.byteLength,
|
|
71
|
+
chunkBase64: Buffer.from(chunk).toString("base64"),
|
|
72
|
+
});
|
|
73
|
+
if (receipt.uploadId !== uploadId)
|
|
74
|
+
throw new MspProtocolError("media/upload");
|
|
75
|
+
}
|
|
76
|
+
result.push({ type: "video", uploadId });
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function eventTurnId(notification, itemTurnIds) {
|
|
81
|
+
const params = notification.params ?? {};
|
|
82
|
+
if (typeof params.turnId === "string")
|
|
83
|
+
return params.turnId;
|
|
84
|
+
const item = params.item;
|
|
85
|
+
if (typeof item?.itemId === "string" && typeof item.turnId === "string")
|
|
86
|
+
itemTurnIds.set(item.itemId, item.turnId);
|
|
87
|
+
if (typeof item?.turnId === "string")
|
|
88
|
+
return item.turnId;
|
|
89
|
+
if (notification.method === "item/delta" && typeof params.itemId === "string")
|
|
90
|
+
return itemTurnIds.get(params.itemId);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
async function* pageGapNotifications(host, sessionId, gap) {
|
|
94
|
+
let cursor = gap.after;
|
|
95
|
+
const requestedCursors = new Set([gap.after]);
|
|
96
|
+
const eventCursors = new Set();
|
|
97
|
+
for (let pageNumber = 0; pageNumber < 100; pageNumber += 1) {
|
|
98
|
+
const requestCursor = cursor;
|
|
99
|
+
const page = await host.request("view/page", {
|
|
100
|
+
sessionId,
|
|
101
|
+
cursor,
|
|
102
|
+
direction: "forward",
|
|
103
|
+
limit: 1000,
|
|
104
|
+
});
|
|
105
|
+
if (!Array.isArray(page.events))
|
|
106
|
+
throw new MspProtocolError("view/page");
|
|
107
|
+
for (const event of page.events) {
|
|
108
|
+
if (typeof event !== "object" || event === null)
|
|
109
|
+
throw new MspProtocolError("view/page");
|
|
110
|
+
const record = event;
|
|
111
|
+
const params = record.params;
|
|
112
|
+
if (typeof record.method !== "string" || !params || params.sessionId !== sessionId || typeof params.viewCursor !== "string") {
|
|
113
|
+
throw new MspProtocolError("view/page");
|
|
114
|
+
}
|
|
115
|
+
if (params.viewCursor === gap.next)
|
|
116
|
+
return;
|
|
117
|
+
if (eventCursors.has(params.viewCursor))
|
|
118
|
+
throw new MspProtocolError("view/page");
|
|
119
|
+
eventCursors.add(params.viewCursor);
|
|
120
|
+
cursor = params.viewCursor;
|
|
121
|
+
yield { jsonrpc: "2.0", method: record.method, params };
|
|
122
|
+
}
|
|
123
|
+
if (page.nextCursor === null || typeof page.nextCursor !== "string"
|
|
124
|
+
|| page.nextCursor === requestCursor || requestedCursors.has(page.nextCursor)) {
|
|
125
|
+
throw new MspProtocolError("view/page");
|
|
126
|
+
}
|
|
127
|
+
requestedCursors.add(page.nextCursor);
|
|
128
|
+
cursor = page.nextCursor;
|
|
129
|
+
}
|
|
130
|
+
throw new MspProtocolError("view/page");
|
|
131
|
+
}
|
|
132
|
+
export async function runMuseTurn(context, input, options, runtime, transcriptFingerprints) {
|
|
133
|
+
const session = await runtime.resolveSession(context);
|
|
134
|
+
let terminalSeen = false;
|
|
135
|
+
let accepted = false;
|
|
136
|
+
let abortRequested = options.abortSignal?.aborted ?? false;
|
|
137
|
+
let cancelPromise;
|
|
138
|
+
let turnId;
|
|
139
|
+
let disposition;
|
|
140
|
+
let subscription;
|
|
141
|
+
let firstNotification;
|
|
142
|
+
let subscriptionController;
|
|
143
|
+
const cancel = () => {
|
|
144
|
+
if (cancelPromise)
|
|
145
|
+
return cancelPromise;
|
|
146
|
+
if (!turnId || !disposition) {
|
|
147
|
+
abortRequested = true;
|
|
148
|
+
return Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
const unqueue = disposition === "queued";
|
|
151
|
+
const method = unqueue ? "turn/unqueue" : "turn/interrupt";
|
|
152
|
+
const params = unqueue
|
|
153
|
+
? { sessionId: session.sessionId, turnId }
|
|
154
|
+
: { sessionId: session.sessionId, turnId, retract: true };
|
|
155
|
+
cancelPromise = submitCommand(session, method, session.host.mintCommandId(), params).then(() => { });
|
|
156
|
+
return cancelPromise;
|
|
157
|
+
};
|
|
158
|
+
const onAbort = () => { abortRequested = true; void cancel().catch(() => { }); };
|
|
159
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
try {
|
|
161
|
+
if (!sameSelection(session.currentSelection, context.museSelection)) {
|
|
162
|
+
const model = {
|
|
163
|
+
providerId: context.museSelection.providerId,
|
|
164
|
+
modelId: context.museSelection.modelId,
|
|
165
|
+
};
|
|
166
|
+
if (context.museSelection.profileId !== null)
|
|
167
|
+
model.profileId = context.museSelection.profileId;
|
|
168
|
+
await submitCommand(session, "session/setModel", session.host.mintCommandId(), {
|
|
169
|
+
sessionId: session.sessionId,
|
|
170
|
+
model,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const inputParts = await mapVideoParts(session, input, options.abortSignal, runtime.videoChunkBytes);
|
|
174
|
+
if (options.abortSignal?.aborted)
|
|
175
|
+
throw new DOMException("Muse request aborted before turn start", "AbortError");
|
|
176
|
+
subscriptionController = new AbortController();
|
|
177
|
+
subscription = session.host.subscribe(session.sessionId, session.replayAfterCursor, subscriptionController.signal)[Symbol.asyncIterator]();
|
|
178
|
+
firstNotification = subscription.next();
|
|
179
|
+
const commandId = session.host.mintCommandId();
|
|
180
|
+
const params = {
|
|
181
|
+
sessionId: session.sessionId,
|
|
182
|
+
input: inputParts,
|
|
183
|
+
};
|
|
184
|
+
if (context.effort)
|
|
185
|
+
params.reasoningEffort = context.effort;
|
|
186
|
+
const ack = await submitCommand(session, "turn/start", commandId, params);
|
|
187
|
+
if (typeof ack.turnId !== "string" || typeof ack.disposition !== "string") {
|
|
188
|
+
throw new MspProtocolError("turn/start");
|
|
189
|
+
}
|
|
190
|
+
turnId = ack.turnId;
|
|
191
|
+
disposition = ack.disposition;
|
|
192
|
+
accepted = true;
|
|
193
|
+
await session.recordTurnAccepted?.(commandId, turnId);
|
|
194
|
+
if (abortRequested)
|
|
195
|
+
await cancel();
|
|
196
|
+
const events = {
|
|
197
|
+
[Symbol.asyncIterator]: async function* () {
|
|
198
|
+
const itemTurnIds = new Map();
|
|
199
|
+
const seenCursors = new Set();
|
|
200
|
+
const processNotification = async function* (notification) {
|
|
201
|
+
const params = notification.params ?? {};
|
|
202
|
+
if (params.sessionId !== session.sessionId)
|
|
203
|
+
return;
|
|
204
|
+
if (typeof params.viewCursor === "string")
|
|
205
|
+
await session.updateCursor?.(params.viewCursor);
|
|
206
|
+
const notificationTurnId = eventTurnId(notification, itemTurnIds);
|
|
207
|
+
if (notificationTurnId !== turnId)
|
|
208
|
+
return;
|
|
209
|
+
yield notification;
|
|
210
|
+
if (notification.method === "turn/completed" || notification.method === "turn/unqueued" || notification.method === "turn/retracted") {
|
|
211
|
+
terminalSeen = true;
|
|
212
|
+
const terminal = notification.method === "turn/completed" && typeof params.terminal === "string"
|
|
213
|
+
? params.terminal
|
|
214
|
+
: notification.method === "turn/unqueued" ? "unqueued" : "retracted";
|
|
215
|
+
await session.recordTurnCompleted?.(turnId, terminal, params.usage);
|
|
216
|
+
if (terminal === "completed" && transcriptFingerprints) {
|
|
217
|
+
await session.commitTranscriptFingerprints?.(transcriptFingerprints);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
let next = await firstNotification;
|
|
223
|
+
while (!next.done) {
|
|
224
|
+
const notification = next.value;
|
|
225
|
+
const params = notification.params ?? {};
|
|
226
|
+
if (notification.method === "view/gap" && params.sessionId === session.sessionId) {
|
|
227
|
+
if (typeof params.after !== "string" || typeof params.next !== "string")
|
|
228
|
+
throw new MspProtocolError("view/gap");
|
|
229
|
+
for await (const replay of pageGapNotifications(session.host, session.sessionId, { after: params.after, next: params.next })) {
|
|
230
|
+
const replayCursor = replay.params?.viewCursor;
|
|
231
|
+
if (typeof replayCursor === "string" && seenCursors.has(replayCursor))
|
|
232
|
+
continue;
|
|
233
|
+
if (typeof replayCursor === "string")
|
|
234
|
+
seenCursors.add(replayCursor);
|
|
235
|
+
for await (const output of processNotification(replay))
|
|
236
|
+
yield output;
|
|
237
|
+
if (terminalSeen)
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const viewCursor = params.viewCursor;
|
|
243
|
+
if (typeof viewCursor === "string" && seenCursors.has(viewCursor)) {
|
|
244
|
+
next = await subscription.next();
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (typeof viewCursor === "string")
|
|
248
|
+
seenCursors.add(viewCursor);
|
|
249
|
+
for await (const output of processNotification(notification))
|
|
250
|
+
yield output;
|
|
251
|
+
if (terminalSeen)
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
next = await subscription.next();
|
|
255
|
+
}
|
|
256
|
+
if (!terminalSeen)
|
|
257
|
+
throw new MspTransportError();
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
261
|
+
subscriptionController?.abort();
|
|
262
|
+
await subscription?.return?.();
|
|
263
|
+
await session.release?.();
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
return { turnId, commandId, disposition, events, cancel };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
271
|
+
if (accepted && subscription && firstNotification && turnId) {
|
|
272
|
+
// The turn was accepted but its local ack could not be durably recorded.
|
|
273
|
+
// Stop it, then keep draining until the terminal event before releasing
|
|
274
|
+
// the workspace lease; dropping this subscription would orphan a writer.
|
|
275
|
+
try {
|
|
276
|
+
await cancel();
|
|
277
|
+
}
|
|
278
|
+
catch { /* continue monitoring even if interrupt admission failed */ }
|
|
279
|
+
void (async () => {
|
|
280
|
+
const itemTurnIds = new Map();
|
|
281
|
+
try {
|
|
282
|
+
let next = await firstNotification.catch(() => ({ done: true, value: undefined }));
|
|
283
|
+
while (!next.done) {
|
|
284
|
+
const notification = next.value;
|
|
285
|
+
const params = notification.params ?? {};
|
|
286
|
+
if (eventTurnId(notification, itemTurnIds) === turnId) {
|
|
287
|
+
if (typeof params.viewCursor === "string") {
|
|
288
|
+
await session.updateCursor?.(params.viewCursor).catch(() => { });
|
|
289
|
+
}
|
|
290
|
+
if (["turn/completed", "turn/unqueued", "turn/retracted"].includes(notification.method)) {
|
|
291
|
+
const terminal = notification.method === "turn/completed" && typeof params.terminal === "string"
|
|
292
|
+
? params.terminal
|
|
293
|
+
: notification.method === "turn/unqueued" ? "unqueued" : "retracted";
|
|
294
|
+
await session.recordTurnCompleted?.(turnId, terminal, params.usage).catch(() => { });
|
|
295
|
+
if (terminal === "completed" && transcriptFingerprints) {
|
|
296
|
+
await session.commitTranscriptFingerprints?.(transcriptFingerprints).catch(() => { });
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
next = await subscription.next();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// The host may have died; its manager will see the unreconciled state.
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
subscriptionController?.abort();
|
|
309
|
+
await subscription?.return?.().catch(() => { });
|
|
310
|
+
await session.release?.().catch(() => { });
|
|
311
|
+
}
|
|
312
|
+
})();
|
|
313
|
+
}
|
|
314
|
+
else if (!accepted) {
|
|
315
|
+
subscriptionController?.abort();
|
|
316
|
+
await firstNotification?.catch(() => undefined);
|
|
317
|
+
await subscription?.return?.();
|
|
318
|
+
const commandMethod = error?.method;
|
|
319
|
+
const ambiguousTurnStart = commandMethod === "turn/start" && !(error instanceof MspCommandRejectedError);
|
|
320
|
+
if (!ambiguousTurnStart)
|
|
321
|
+
await session.release?.();
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function contextErrorToException(error) {
|
|
327
|
+
return new MuseBridgeError("MUSE_UNSUPPORTED_REQUEST", `Muse model request context is ${error.code} (${error.field})`, false);
|
|
328
|
+
}
|
|
329
|
+
function inputErrorToException(error) {
|
|
330
|
+
if (error.code === "transcript-diverged")
|
|
331
|
+
return new MuseTranscriptDivergedError();
|
|
332
|
+
return new MuseUnsupportedInputError(error.field ?? error.mediaType ?? error.code);
|
|
333
|
+
}
|
|
334
|
+
function ignoredOptionWarnings(ignoredOptions) {
|
|
335
|
+
return ignoredOptions.map((feature) => ({
|
|
336
|
+
type: "unsupported",
|
|
337
|
+
feature,
|
|
338
|
+
details: "Muse MSP does not expose this per-turn option; the setting was ignored.",
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
function resultUsage(usage) {
|
|
342
|
+
return {
|
|
343
|
+
inputTokens: usage?.inputTokens,
|
|
344
|
+
outputTokens: usage?.outputTokens,
|
|
345
|
+
totalTokens: usage?.totalTokens,
|
|
346
|
+
reasoningTokens: usage?.reasoningTokens,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function requestKindFromHeaders(options) {
|
|
350
|
+
const headers = options.headers;
|
|
351
|
+
if (!headers)
|
|
352
|
+
return undefined;
|
|
353
|
+
const name = MUSE_CONTEXT_HEADERS.kind.toLowerCase();
|
|
354
|
+
for (const [key, value] of Object.entries(headers))
|
|
355
|
+
if (key.toLowerCase() === name)
|
|
356
|
+
return value;
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
function classifyCall(options) {
|
|
360
|
+
return classifyMetaRequest(requestKindFromHeaders(options), options.prompt);
|
|
361
|
+
}
|
|
362
|
+
function localMetaText(kind, options) {
|
|
363
|
+
return kind === "title"
|
|
364
|
+
? makeLocalTitle(options.prompt)
|
|
365
|
+
: makeCompactionDigest(options.prompt, options.maxOutputTokens);
|
|
366
|
+
}
|
|
367
|
+
function localMetaGenerateResult(kind, text) {
|
|
368
|
+
return {
|
|
369
|
+
content: text ? [{ type: "text", text }] : [],
|
|
370
|
+
finishReason: { unified: "stop", raw: `local-${kind}` },
|
|
371
|
+
usage: resultUsage(undefined),
|
|
372
|
+
warnings: [],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function localMetaStream(kind, text) {
|
|
376
|
+
return new ReadableStream({
|
|
377
|
+
start(controller) {
|
|
378
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
379
|
+
if (text) {
|
|
380
|
+
controller.enqueue({ type: "text-start", id: `local-${kind}` });
|
|
381
|
+
controller.enqueue({ type: "text-delta", id: `local-${kind}`, delta: text });
|
|
382
|
+
controller.enqueue({ type: "text-end", id: `local-${kind}` });
|
|
383
|
+
}
|
|
384
|
+
controller.enqueue({
|
|
385
|
+
type: "finish",
|
|
386
|
+
finishReason: { unified: "stop", raw: `local-${kind}` },
|
|
387
|
+
usage: resultUsage(undefined),
|
|
388
|
+
});
|
|
389
|
+
controller.close();
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
export function createMuseLanguageModel(runtime, modelID) {
|
|
394
|
+
const doPrepare = async (options) => {
|
|
395
|
+
const requestKind = classifyCall(options);
|
|
396
|
+
if (requestKind !== "agent")
|
|
397
|
+
throw new MuseUnsupportedRequestError(requestKind);
|
|
398
|
+
const context = readRequestContext(options, runtime.selectionIndex);
|
|
399
|
+
if ("type" in context)
|
|
400
|
+
throw contextErrorToException(context);
|
|
401
|
+
if (context.kind !== "primary")
|
|
402
|
+
throw new MuseUnsupportedRequestError(context.kind);
|
|
403
|
+
const supportedOptions = validateModelOptions(options);
|
|
404
|
+
if (!supportedOptions.ok)
|
|
405
|
+
throw inputErrorToException(supportedOptions.error);
|
|
406
|
+
const initialExtraction = extractTurnInput(options.prompt, [], [], { signal: options.abortSignal });
|
|
407
|
+
if (!initialExtraction.ok)
|
|
408
|
+
throw inputErrorToException(initialExtraction.error);
|
|
409
|
+
const session = await runtime.resolveSession(context);
|
|
410
|
+
if (session.contextError) {
|
|
411
|
+
await session.release?.();
|
|
412
|
+
throw new MuseUnsupportedRequestError(session.contextError);
|
|
413
|
+
}
|
|
414
|
+
const observedCompaction = (session.compactionFingerprints ?? []).some((candidate) => promptContainsCompactionFingerprint(options.prompt, candidate));
|
|
415
|
+
const extracted = extractTurnInput(options.prompt, observedCompaction ? [] : session.previousTranscriptFingerprints, session.contextAllowlist, { signal: options.abortSignal });
|
|
416
|
+
if (!extracted.ok) {
|
|
417
|
+
await session.release?.();
|
|
418
|
+
throw inputErrorToException(extracted.error);
|
|
419
|
+
}
|
|
420
|
+
const execution = await runMuseTurn(context, extracted.parts, options, {
|
|
421
|
+
...runtime,
|
|
422
|
+
resolveSession: async () => session,
|
|
423
|
+
}, extracted.nextTranscriptFingerprints);
|
|
424
|
+
return { context, execution, mapper: new MuseStreamMapper(execution.turnId), warnings: ignoredOptionWarnings(supportedOptions.options.ignoredOptions) };
|
|
425
|
+
};
|
|
426
|
+
return {
|
|
427
|
+
specificationVersion: "v3",
|
|
428
|
+
// OpenCode 2.1.2's proven in-process hook consumes specVersion and flat
|
|
429
|
+
// usage; keep the official SDK type member as well for type-level V3.
|
|
430
|
+
specVersion: "v3",
|
|
431
|
+
provider: "muse",
|
|
432
|
+
modelId: modelID,
|
|
433
|
+
supportedUrls: {},
|
|
434
|
+
async doStream(options) {
|
|
435
|
+
const requestKind = classifyCall(options);
|
|
436
|
+
if (requestKind === "title" || requestKind === "compaction") {
|
|
437
|
+
return { stream: localMetaStream(requestKind, localMetaText(requestKind, options)) };
|
|
438
|
+
}
|
|
439
|
+
if (requestKind === "unsupported-generate")
|
|
440
|
+
throw new MuseUnsupportedRequestError("generate");
|
|
441
|
+
const prepared = await doPrepare(options);
|
|
442
|
+
let outputClosed = false;
|
|
443
|
+
let controllerRef;
|
|
444
|
+
const closeOutput = () => {
|
|
445
|
+
if (outputClosed)
|
|
446
|
+
return;
|
|
447
|
+
outputClosed = true;
|
|
448
|
+
try {
|
|
449
|
+
controllerRef?.close();
|
|
450
|
+
}
|
|
451
|
+
catch { /* stream was cancelled by OpenCode */ }
|
|
452
|
+
};
|
|
453
|
+
const stream = new ReadableStream({
|
|
454
|
+
start(controller) {
|
|
455
|
+
controllerRef = controller;
|
|
456
|
+
controller.enqueue({ type: "stream-start", warnings: prepared.warnings });
|
|
457
|
+
void (async () => {
|
|
458
|
+
try {
|
|
459
|
+
for await (const notification of prepared.execution.events) {
|
|
460
|
+
const parts = prepared.mapper.accept(notification);
|
|
461
|
+
for (const part of parts) {
|
|
462
|
+
if (outputClosed)
|
|
463
|
+
continue;
|
|
464
|
+
if (part.type === "finish") {
|
|
465
|
+
controller.enqueue(part);
|
|
466
|
+
closeOutput();
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
controller.enqueue(part);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (!outputClosed)
|
|
473
|
+
closeOutput();
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
if (outputClosed)
|
|
477
|
+
return;
|
|
478
|
+
const safeError = error instanceof MuseBridgeError ? error : new MspTransportError();
|
|
479
|
+
controller.enqueue({ type: "error", error: safeError });
|
|
480
|
+
controller.enqueue({
|
|
481
|
+
type: "finish",
|
|
482
|
+
finishReason: { unified: "error", raw: undefined },
|
|
483
|
+
usage: resultUsage(prepared.mapper.usage),
|
|
484
|
+
});
|
|
485
|
+
closeOutput();
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
},
|
|
489
|
+
async cancel() {
|
|
490
|
+
outputClosed = true;
|
|
491
|
+
await prepared.execution.cancel();
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
return { stream };
|
|
495
|
+
},
|
|
496
|
+
async doGenerate(options) {
|
|
497
|
+
const requestKind = classifyCall(options);
|
|
498
|
+
if (requestKind === "title" || requestKind === "compaction") {
|
|
499
|
+
return localMetaGenerateResult(requestKind, localMetaText(requestKind, options));
|
|
500
|
+
}
|
|
501
|
+
if (requestKind === "unsupported-generate")
|
|
502
|
+
throw new MuseUnsupportedRequestError("generate");
|
|
503
|
+
const prepared = await doPrepare(options);
|
|
504
|
+
let terminalError;
|
|
505
|
+
const iterator = prepared.execution.events[Symbol.asyncIterator]();
|
|
506
|
+
while (true) {
|
|
507
|
+
const next = await iterator.next();
|
|
508
|
+
if (next.done)
|
|
509
|
+
break;
|
|
510
|
+
const parts = prepared.mapper.accept(next.value);
|
|
511
|
+
if (prepared.mapper.failure)
|
|
512
|
+
terminalError = prepared.mapper.failure;
|
|
513
|
+
const foregroundFinished = parts.some((part) => part.type === "finish"
|
|
514
|
+
&& part.finishReason?.raw === "foregroundCompleted");
|
|
515
|
+
if (foregroundFinished) {
|
|
516
|
+
void (async () => {
|
|
517
|
+
try {
|
|
518
|
+
while (!(await iterator.next()).done) {
|
|
519
|
+
// Continue monitoring terminal state and release the workspace lease.
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// The foreground response is already complete; terminal monitor
|
|
524
|
+
// failure is recorded by the session manager's durable state.
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (terminalError)
|
|
531
|
+
throw terminalError;
|
|
532
|
+
return {
|
|
533
|
+
content: prepared.mapper.content,
|
|
534
|
+
finishReason: prepared.mapper.finishReason ?? { unified: "other", raw: undefined },
|
|
535
|
+
usage: resultUsage(prepared.mapper.usage),
|
|
536
|
+
warnings: prepared.warnings,
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LanguageModelV3Prompt } from "@ai-sdk/provider";
|
|
2
|
+
export type MetaRequestClass = "title" | "compaction" | "unsupported-generate" | "agent";
|
|
3
|
+
interface PromptMessageLike {
|
|
4
|
+
role?: string;
|
|
5
|
+
content?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** Prefer the correlated kind; prompt-shape fallback is intentionally narrow. */
|
|
8
|
+
export declare function classifyMetaRequest(kind: string | undefined, prompt: readonly PromptMessageLike[] | LanguageModelV3Prompt): MetaRequestClass;
|
|
9
|
+
/** Local, zero-model-call title from the first user-authored message only. */
|
|
10
|
+
export declare function makeLocalTitle(prompt: readonly PromptMessageLike[] | LanguageModelV3Prompt): string;
|
|
11
|
+
/**
|
|
12
|
+
* Deterministic local transcript digest. It is bookkeeping, not synthesis:
|
|
13
|
+
* no tools, system prompts, hidden reasoning, or inferred decisions are added.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeCompactionDigest(prompt: readonly PromptMessageLike[] | LanguageModelV3Prompt, maxOutputTokens?: number): string;
|
|
16
|
+
export {};
|