opencode-muse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +121 -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 +383 -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,171 @@
|
|
|
1
|
+
const DEFAULT_TITLE = "Muse session";
|
|
2
|
+
const MAX_TITLE_CODEPOINTS = 72;
|
|
3
|
+
const MAX_DIGEST_CODEPOINTS = 12_000;
|
|
4
|
+
const DIGEST_HEADER = "Transcript digest (local heuristic; not semantic):\n";
|
|
5
|
+
const DIGEST_OMISSION = "[Omitted earlier content; no facts inferred.]";
|
|
6
|
+
function promptMessages(prompt) {
|
|
7
|
+
return prompt;
|
|
8
|
+
}
|
|
9
|
+
function textFromContent(content) {
|
|
10
|
+
if (typeof content === "string")
|
|
11
|
+
return content;
|
|
12
|
+
if (!Array.isArray(content))
|
|
13
|
+
return "";
|
|
14
|
+
return content
|
|
15
|
+
.map((part) => {
|
|
16
|
+
if (typeof part !== "object" || part === null)
|
|
17
|
+
return "";
|
|
18
|
+
const record = part;
|
|
19
|
+
return record.type === "text" && typeof record.text === "string" ? record.text : "";
|
|
20
|
+
})
|
|
21
|
+
.filter((text) => text.length > 0)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
function allText(prompt, role) {
|
|
25
|
+
return promptMessages(prompt)
|
|
26
|
+
.filter((message) => role === undefined || message.role === role)
|
|
27
|
+
.map((message) => textFromContent(message.content))
|
|
28
|
+
.filter((text) => text.length > 0)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
function detectPromptKind(prompt) {
|
|
32
|
+
const system = allText(prompt, "system").toLowerCase();
|
|
33
|
+
const user = allText(prompt, "user").toLowerCase();
|
|
34
|
+
if (system.includes("title generator")
|
|
35
|
+
|| system.includes("generate a short title")
|
|
36
|
+
|| system.includes("generate a concise title")
|
|
37
|
+
|| system.includes("output only a thread title"))
|
|
38
|
+
return "title";
|
|
39
|
+
if (system.includes("anchored context summarization")
|
|
40
|
+
|| system.includes("summarizing, compacting, or merging context")
|
|
41
|
+
|| system.includes("tasked with summarizing conversations")
|
|
42
|
+
|| system.includes("summarize what was done in this conversation")
|
|
43
|
+
|| user.includes("this summary will be the only context available when the conversation continues")
|
|
44
|
+
|| user.includes("create a detailed summary for continuing this coding session")
|
|
45
|
+
|| user.includes("anchored summary below using the conversation history")
|
|
46
|
+
|| user.includes("<previous-summary>"))
|
|
47
|
+
return "compaction";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Prefer the correlated kind; prompt-shape fallback is intentionally narrow. */
|
|
51
|
+
export function classifyMetaRequest(kind, prompt) {
|
|
52
|
+
if (kind === "title")
|
|
53
|
+
return "title";
|
|
54
|
+
if (kind === "compaction")
|
|
55
|
+
return "compaction";
|
|
56
|
+
if (kind === "primary" || kind === "agent")
|
|
57
|
+
return "agent";
|
|
58
|
+
if (kind === "generate" || kind === undefined || kind.length === 0) {
|
|
59
|
+
const detected = detectPromptKind(prompt);
|
|
60
|
+
if (detected)
|
|
61
|
+
return detected;
|
|
62
|
+
return "unsupported-generate";
|
|
63
|
+
}
|
|
64
|
+
return "unsupported-generate";
|
|
65
|
+
}
|
|
66
|
+
/** Local, zero-model-call title from the first user-authored message only. */
|
|
67
|
+
export function makeLocalTitle(prompt) {
|
|
68
|
+
const firstUser = promptMessages(prompt).find((message) => message.role === "user");
|
|
69
|
+
const text = textFromContent(firstUser?.content).replace(/\s+/g, " ").trim();
|
|
70
|
+
if (!text)
|
|
71
|
+
return DEFAULT_TITLE;
|
|
72
|
+
const points = Array.from(text);
|
|
73
|
+
if (points.length <= MAX_TITLE_CODEPOINTS)
|
|
74
|
+
return text;
|
|
75
|
+
const clipped = points.slice(0, MAX_TITLE_CODEPOINTS - 1).join("").trimEnd();
|
|
76
|
+
const wordBoundary = clipped.lastIndexOf(" ");
|
|
77
|
+
return `${(wordBoundary > MAX_TITLE_CODEPOINTS / 2 ? clipped.slice(0, wordBoundary) : clipped).trimEnd()}…`;
|
|
78
|
+
}
|
|
79
|
+
function digestMessages(prompt) {
|
|
80
|
+
return promptMessages(prompt)
|
|
81
|
+
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
82
|
+
.map((message) => ({ role: message.role, text: textFromContent(message.content).trim() }))
|
|
83
|
+
.filter((message) => message.text.length > 0);
|
|
84
|
+
}
|
|
85
|
+
function codepoints(text) {
|
|
86
|
+
return Array.from(text);
|
|
87
|
+
}
|
|
88
|
+
function truncate(text, maxCodepoints) {
|
|
89
|
+
const points = codepoints(text);
|
|
90
|
+
if (points.length <= maxCodepoints)
|
|
91
|
+
return text;
|
|
92
|
+
if (maxCodepoints <= 1)
|
|
93
|
+
return "…".slice(0, maxCodepoints);
|
|
94
|
+
return `${points.slice(0, maxCodepoints - 1).join("")}…`;
|
|
95
|
+
}
|
|
96
|
+
function completeExchangePairs(messages, initialIndex) {
|
|
97
|
+
const pairs = [];
|
|
98
|
+
let user;
|
|
99
|
+
for (let index = initialIndex + 1; index < messages.length; index += 1) {
|
|
100
|
+
const message = messages[index];
|
|
101
|
+
if (message.role === "user") {
|
|
102
|
+
user = message;
|
|
103
|
+
}
|
|
104
|
+
else if (user) {
|
|
105
|
+
pairs.push([user, message]);
|
|
106
|
+
user = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return pairs;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Deterministic local transcript digest. It is bookkeeping, not synthesis:
|
|
113
|
+
* no tools, system prompts, hidden reasoning, or inferred decisions are added.
|
|
114
|
+
*/
|
|
115
|
+
export function makeCompactionDigest(prompt, maxOutputTokens) {
|
|
116
|
+
const tokenBudget = maxOutputTokens === undefined || !Number.isFinite(maxOutputTokens)
|
|
117
|
+
? 1_024
|
|
118
|
+
: Math.floor(maxOutputTokens);
|
|
119
|
+
const maxCodepoints = Math.min(MAX_DIGEST_CODEPOINTS, tokenBudget);
|
|
120
|
+
const messages = digestMessages(prompt);
|
|
121
|
+
if (maxCodepoints < DIGEST_HEADER.length + DIGEST_OMISSION.length + 8) {
|
|
122
|
+
throw new Error("Compaction output budget is too small for a truthful labeled digest");
|
|
123
|
+
}
|
|
124
|
+
if (messages.length === 0) {
|
|
125
|
+
const empty = `${DIGEST_HEADER}No user/assistant text was available.`;
|
|
126
|
+
if (codepoints(empty).length > maxCodepoints)
|
|
127
|
+
throw new Error("Compaction output budget is too small for a truthful labeled digest");
|
|
128
|
+
return empty;
|
|
129
|
+
}
|
|
130
|
+
const initialIndex = messages.findIndex((message) => message.role === "user");
|
|
131
|
+
const firstIndex = initialIndex >= 0 ? initialIndex : 0;
|
|
132
|
+
const initial = messages[firstIndex];
|
|
133
|
+
const initialBlock = `${initialIndex >= 0 ? "Initial request" : "Initial visible message"}: ${initial.text}`;
|
|
134
|
+
const pairs = completeExchangePairs(messages, firstIndex);
|
|
135
|
+
const completeLines = [initialBlock, ...pairs.map(([user, assistant]) => `User: ${user.text}\nAssistant: ${assistant.text}`)];
|
|
136
|
+
const complete = `${DIGEST_HEADER}${completeLines.join("\n\n")}`;
|
|
137
|
+
if (codepoints(complete).length <= maxCodepoints)
|
|
138
|
+
return complete;
|
|
139
|
+
const fixedCost = codepoints(DIGEST_HEADER).length + codepoints(DIGEST_OMISSION).length + 2;
|
|
140
|
+
const bodyBudget = maxCodepoints - fixedCost;
|
|
141
|
+
const initialLabel = `${initialIndex >= 0 ? "Initial request" : "Initial visible message"}: `;
|
|
142
|
+
const initialContentBudget = Math.min(codepoints(initial.text).length, Math.max(1, Math.floor(bodyBudget * 0.2)));
|
|
143
|
+
const selected = [initialLabel + truncate(initial.text, initialContentBudget)];
|
|
144
|
+
let remaining = bodyBudget - codepoints(selected[0]).length;
|
|
145
|
+
const selectedPairs = [];
|
|
146
|
+
for (let index = pairs.length - 1; index >= 0; index -= 1) {
|
|
147
|
+
const [user, assistant] = pairs[index];
|
|
148
|
+
const block = `User: ${user.text}\nAssistant: ${assistant.text}`;
|
|
149
|
+
const separatorCost = 2;
|
|
150
|
+
const cost = codepoints(block).length + separatorCost;
|
|
151
|
+
if (cost > remaining) {
|
|
152
|
+
if (selectedPairs.length === 0 && remaining > 32) {
|
|
153
|
+
const prefixCost = codepoints("User: \nAssistant: ").length + separatorCost;
|
|
154
|
+
const textBudget = remaining - prefixCost;
|
|
155
|
+
const userBudget = Math.floor(textBudget / 2);
|
|
156
|
+
const assistantBudget = textBudget - userBudget;
|
|
157
|
+
selectedPairs.unshift(`User: ${truncate(user.text, userBudget)}\nAssistant: ${truncate(assistant.text, assistantBudget)}`);
|
|
158
|
+
remaining = 0;
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
selectedPairs.unshift(block);
|
|
163
|
+
remaining -= cost;
|
|
164
|
+
}
|
|
165
|
+
selected.push(...selectedPairs);
|
|
166
|
+
const result = `${DIGEST_HEADER}${selected.join("\n\n")}\n${DIGEST_OMISSION}`;
|
|
167
|
+
if (codepoints(result).length > maxCodepoints) {
|
|
168
|
+
throw new Error("Compaction output budget is too small for a truthful labeled digest");
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LanguageModelV3Prompt } from "@ai-sdk/provider";
|
|
2
|
+
export interface ContextFragment {
|
|
3
|
+
label: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export type ExtractedTurnPart = {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "image";
|
|
11
|
+
base64Data: string;
|
|
12
|
+
mediaType: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "video";
|
|
15
|
+
base64Data: string;
|
|
16
|
+
mediaType: "video/mp4" | "video/quicktime";
|
|
17
|
+
totalBytes: number;
|
|
18
|
+
};
|
|
19
|
+
export interface ExtractedTurnInput {
|
|
20
|
+
parts: ExtractedTurnPart[];
|
|
21
|
+
nextTranscriptFingerprints: string[];
|
|
22
|
+
}
|
|
23
|
+
export type PromptInputErrorCode = "transcript-diverged" | "no-new-user-input" | "invalid-context" | "invalid-media" | "remote-media" | "unsupported-media" | "media-too-large" | "aborted" | "unsupported-response-format" | "required-opencode-tool" | "unsupported-generation-option";
|
|
24
|
+
export interface PromptInputError {
|
|
25
|
+
type: "prompt-error";
|
|
26
|
+
code: PromptInputErrorCode;
|
|
27
|
+
field?: string;
|
|
28
|
+
mediaType?: string;
|
|
29
|
+
}
|
|
30
|
+
export type ExtractTurnInputResult = ({
|
|
31
|
+
ok: true;
|
|
32
|
+
} & ExtractedTurnInput) | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: PromptInputError;
|
|
35
|
+
};
|
|
36
|
+
export interface ExtractTurnInputOptions {
|
|
37
|
+
maxMediaBytes?: number;
|
|
38
|
+
maxInlineImageBytes?: number;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
export interface ModelOptionsInput {
|
|
42
|
+
responseFormat?: {
|
|
43
|
+
type?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
toolChoice?: string | {
|
|
47
|
+
type?: string;
|
|
48
|
+
toolName?: string;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
tools?: readonly unknown[];
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
export type ModelOptionsResult = {
|
|
55
|
+
ok: true;
|
|
56
|
+
options: {
|
|
57
|
+
toolsIgnored: boolean;
|
|
58
|
+
ignoredOptions: string[];
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
error: PromptInputError;
|
|
63
|
+
};
|
|
64
|
+
export declare const DEFAULT_MAX_MEDIA_BYTES: number;
|
|
65
|
+
export declare const DEFAULT_MAX_INLINE_IMAGE_BYTES: number;
|
|
66
|
+
export declare function fingerprintVisibleTranscript(prompt: LanguageModelV3Prompt): string[];
|
|
67
|
+
export declare function extractTurnInput(prompt: LanguageModelV3Prompt, previousTranscriptFingerprints: readonly string[], contextAllowlist: readonly ContextFragment[], options?: ExtractTurnInputOptions): ExtractTurnInputResult;
|
|
68
|
+
export declare function validateModelOptions(options: ModelOptionsInput): ModelOptionsResult;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { fingerprintTranscript } from "../session/fingerprint.js";
|
|
2
|
+
export const DEFAULT_MAX_MEDIA_BYTES = 64 * 1024 * 1024;
|
|
3
|
+
export const DEFAULT_MAX_INLINE_IMAGE_BYTES = 7 * 1024 * 1024;
|
|
4
|
+
function inputError(code, fields = {}) {
|
|
5
|
+
return { type: "prompt-error", code, ...fields };
|
|
6
|
+
}
|
|
7
|
+
function safeMediaType(value) {
|
|
8
|
+
const normalized = value.toLowerCase();
|
|
9
|
+
return /^[a-z0-9.+*-]+\/[a-z0-9.+*-]+$/.test(normalized) ? normalized : "unknown";
|
|
10
|
+
}
|
|
11
|
+
export function fingerprintVisibleTranscript(prompt) {
|
|
12
|
+
return fingerprintTranscript(prompt);
|
|
13
|
+
}
|
|
14
|
+
function visibleMessages(prompt) {
|
|
15
|
+
const result = [];
|
|
16
|
+
for (const message of prompt) {
|
|
17
|
+
const fingerprint = fingerprintTranscript([message])[0];
|
|
18
|
+
if (fingerprint)
|
|
19
|
+
result.push({ message, fingerprint });
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function decodeBase64(value) {
|
|
24
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 === 1)
|
|
25
|
+
return undefined;
|
|
26
|
+
const bytes = Buffer.from(value, "base64");
|
|
27
|
+
const canonical = bytes.toString("base64").replace(/=+$/, "");
|
|
28
|
+
return canonical === value.replace(/=+$/, "") ? bytes : undefined;
|
|
29
|
+
}
|
|
30
|
+
function fileBytes(part) {
|
|
31
|
+
let data = part.data;
|
|
32
|
+
if (data instanceof URL) {
|
|
33
|
+
if (data.protocol !== "data:")
|
|
34
|
+
return inputError("remote-media", { mediaType: safeMediaType(part.mediaType) });
|
|
35
|
+
data = data.href;
|
|
36
|
+
}
|
|
37
|
+
if (typeof data !== "string")
|
|
38
|
+
return { bytes: data, mediaType: part.mediaType };
|
|
39
|
+
if (/^https?:\/\//i.test(data))
|
|
40
|
+
return inputError("remote-media", { mediaType: safeMediaType(part.mediaType) });
|
|
41
|
+
let encoded = data;
|
|
42
|
+
let mediaType = part.mediaType;
|
|
43
|
+
if (data.startsWith("data:")) {
|
|
44
|
+
const match = /^data:([^;,]+);base64,(.*)$/s.exec(data);
|
|
45
|
+
if (!match)
|
|
46
|
+
return inputError("invalid-media", { mediaType: safeMediaType(part.mediaType) });
|
|
47
|
+
if (match[1] !== part.mediaType && part.mediaType !== "*/*") {
|
|
48
|
+
return inputError("invalid-media", { mediaType: safeMediaType(part.mediaType) });
|
|
49
|
+
}
|
|
50
|
+
mediaType = match[1];
|
|
51
|
+
encoded = match[2];
|
|
52
|
+
}
|
|
53
|
+
const bytes = decodeBase64(encoded);
|
|
54
|
+
if (!bytes || bytes.byteLength === 0)
|
|
55
|
+
return inputError("invalid-media", { mediaType: safeMediaType(mediaType) });
|
|
56
|
+
return { bytes, mediaType };
|
|
57
|
+
}
|
|
58
|
+
function extractFilePart(part, limits) {
|
|
59
|
+
const mediaType = part.mediaType.toLowerCase();
|
|
60
|
+
if (mediaType.startsWith("audio/") || mediaType === "application/pdf") {
|
|
61
|
+
return inputError("unsupported-media", { mediaType: safeMediaType(mediaType) });
|
|
62
|
+
}
|
|
63
|
+
const decoded = fileBytes(part);
|
|
64
|
+
if ("type" in decoded)
|
|
65
|
+
return decoded;
|
|
66
|
+
if (decoded.bytes.byteLength > limits.maxMediaBytes)
|
|
67
|
+
return inputError("media-too-large", { mediaType: safeMediaType(mediaType) });
|
|
68
|
+
if (mediaType.startsWith("image/")) {
|
|
69
|
+
if (!new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]).has(mediaType)) {
|
|
70
|
+
return inputError("unsupported-media", { mediaType: safeMediaType(mediaType) });
|
|
71
|
+
}
|
|
72
|
+
if (decoded.bytes.byteLength > limits.maxInlineImageBytes)
|
|
73
|
+
return inputError("media-too-large", { mediaType: safeMediaType(mediaType) });
|
|
74
|
+
return { type: "image", base64Data: Buffer.from(decoded.bytes).toString("base64"), mediaType };
|
|
75
|
+
}
|
|
76
|
+
if (mediaType === "video/mp4" || mediaType === "video/quicktime") {
|
|
77
|
+
return {
|
|
78
|
+
type: "video",
|
|
79
|
+
base64Data: Buffer.from(decoded.bytes).toString("base64"),
|
|
80
|
+
mediaType,
|
|
81
|
+
totalBytes: decoded.bytes.byteLength,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (mediaType.startsWith("text/")) {
|
|
85
|
+
return { type: "text", text: Buffer.from(decoded.bytes).toString("utf8") };
|
|
86
|
+
}
|
|
87
|
+
return inputError("unsupported-media", { mediaType: safeMediaType(mediaType) });
|
|
88
|
+
}
|
|
89
|
+
export function extractTurnInput(prompt, previousTranscriptFingerprints, contextAllowlist, options = {}) {
|
|
90
|
+
if (options.signal?.aborted)
|
|
91
|
+
return { ok: false, error: inputError("aborted") };
|
|
92
|
+
const current = visibleMessages(prompt);
|
|
93
|
+
const currentFingerprints = current.map((entry) => entry.fingerprint);
|
|
94
|
+
let newMessages = current;
|
|
95
|
+
if (previousTranscriptFingerprints.length > 0) {
|
|
96
|
+
if (previousTranscriptFingerprints.length > currentFingerprints.length
|
|
97
|
+
|| previousTranscriptFingerprints.some((fingerprint, index) => currentFingerprints[index] !== fingerprint)) {
|
|
98
|
+
return { ok: false, error: inputError("transcript-diverged") };
|
|
99
|
+
}
|
|
100
|
+
newMessages = current.slice(previousTranscriptFingerprints.length);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// On first use in an existing OpenCode session, send only the final user
|
|
104
|
+
// turn; never replay its previous transcript into a new Muse session.
|
|
105
|
+
let lastUserIndex = -1;
|
|
106
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
107
|
+
if (current[index].message.role === "user")
|
|
108
|
+
lastUserIndex = index;
|
|
109
|
+
}
|
|
110
|
+
if (lastUserIndex >= 0)
|
|
111
|
+
newMessages = current.slice(lastUserIndex);
|
|
112
|
+
else
|
|
113
|
+
newMessages = [];
|
|
114
|
+
}
|
|
115
|
+
const currentUserMessages = newMessages.filter((entry) => entry.message.role === "user");
|
|
116
|
+
if (currentUserMessages.length === 0)
|
|
117
|
+
return { ok: false, error: inputError("no-new-user-input") };
|
|
118
|
+
const parts = [];
|
|
119
|
+
for (const fragment of contextAllowlist) {
|
|
120
|
+
if (options.signal?.aborted)
|
|
121
|
+
return { ok: false, error: inputError("aborted") };
|
|
122
|
+
if (!fragment.label.trim() || !fragment.text.trim())
|
|
123
|
+
return { ok: false, error: inputError("invalid-context") };
|
|
124
|
+
parts.push({ type: "text", text: `[${fragment.label.trim()}]\n${fragment.text}` });
|
|
125
|
+
}
|
|
126
|
+
const limits = {
|
|
127
|
+
maxMediaBytes: options.maxMediaBytes ?? DEFAULT_MAX_MEDIA_BYTES,
|
|
128
|
+
maxInlineImageBytes: options.maxInlineImageBytes ?? DEFAULT_MAX_INLINE_IMAGE_BYTES,
|
|
129
|
+
};
|
|
130
|
+
for (const entry of currentUserMessages) {
|
|
131
|
+
if (entry.message.role !== "user")
|
|
132
|
+
continue;
|
|
133
|
+
for (const part of entry.message.content) {
|
|
134
|
+
if (options.signal?.aborted)
|
|
135
|
+
return { ok: false, error: inputError("aborted") };
|
|
136
|
+
if (part.type === "text") {
|
|
137
|
+
parts.push({ type: "text", text: part.text });
|
|
138
|
+
}
|
|
139
|
+
else if (part.type === "file") {
|
|
140
|
+
const mapped = extractFilePart(part, limits);
|
|
141
|
+
if ("code" in mapped)
|
|
142
|
+
return { ok: false, error: mapped };
|
|
143
|
+
parts.push(mapped);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (options.signal?.aborted)
|
|
148
|
+
return { ok: false, error: inputError("aborted") };
|
|
149
|
+
if (parts.length === 0)
|
|
150
|
+
return { ok: false, error: inputError("no-new-user-input") };
|
|
151
|
+
return { ok: true, parts, nextTranscriptFingerprints: currentFingerprints };
|
|
152
|
+
}
|
|
153
|
+
export function validateModelOptions(options) {
|
|
154
|
+
if (options.responseFormat?.type === "json") {
|
|
155
|
+
return { ok: false, error: inputError("unsupported-response-format", { field: "responseFormat" }) };
|
|
156
|
+
}
|
|
157
|
+
if (options.responseFormat !== undefined && options.responseFormat.type !== "text") {
|
|
158
|
+
return { ok: false, error: inputError("unsupported-response-format", { field: "responseFormat" }) };
|
|
159
|
+
}
|
|
160
|
+
const toolChoice = options.toolChoice;
|
|
161
|
+
if (typeof toolChoice === "object" && toolChoice !== null
|
|
162
|
+
&& (toolChoice.type === "tool" || toolChoice.type === "required")) {
|
|
163
|
+
return { ok: false, error: inputError("required-opencode-tool", { field: "toolChoice" }) };
|
|
164
|
+
}
|
|
165
|
+
if (toolChoice === "required")
|
|
166
|
+
return { ok: false, error: inputError("required-opencode-tool", { field: "toolChoice" }) };
|
|
167
|
+
const samplingOptions = [
|
|
168
|
+
"maxOutputTokens", "temperature", "topP", "topK", "presencePenalty", "frequencyPenalty", "stopSequences", "seed", "providerOptions",
|
|
169
|
+
];
|
|
170
|
+
const ignoredOptions = samplingOptions.filter((name) => options[name] !== undefined);
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
options: { toolsIgnored: (options.tools?.length ?? 0) > 0, ignoredOptions },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MspNotification } from "../msp/client.js";
|
|
2
|
+
export interface FlatMuseUsage {
|
|
3
|
+
inputTokens: number | undefined;
|
|
4
|
+
outputTokens: number | undefined;
|
|
5
|
+
totalTokens: number | undefined;
|
|
6
|
+
reasoningTokens: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export type MuseStreamPart = Record<string, unknown>;
|
|
9
|
+
/** Maps typed Muse view facts to text/reasoning-only V3 events and safe usage. */
|
|
10
|
+
export declare class MuseStreamMapper {
|
|
11
|
+
readonly turnId: string;
|
|
12
|
+
private readonly itemKinds;
|
|
13
|
+
private readonly itemStarted;
|
|
14
|
+
private readonly reasoningStarted;
|
|
15
|
+
private readonly itemDeltaCounts;
|
|
16
|
+
private readonly itemText;
|
|
17
|
+
private readonly seenUsageCursors;
|
|
18
|
+
private readonly textParts;
|
|
19
|
+
private terminalUsage;
|
|
20
|
+
private callUsage;
|
|
21
|
+
private hasCallUsage;
|
|
22
|
+
private finishWasSent;
|
|
23
|
+
private currentFinishReason;
|
|
24
|
+
terminal?: string;
|
|
25
|
+
failure?: Error;
|
|
26
|
+
constructor(turnId: string);
|
|
27
|
+
get generatedText(): string;
|
|
28
|
+
get content(): Array<{
|
|
29
|
+
type: "text";
|
|
30
|
+
text: string;
|
|
31
|
+
}>;
|
|
32
|
+
get usage(): FlatMuseUsage | undefined;
|
|
33
|
+
get finishWasSentToOpenCode(): boolean;
|
|
34
|
+
get finishReason(): {
|
|
35
|
+
unified: "stop" | "error" | "other";
|
|
36
|
+
raw: string;
|
|
37
|
+
} | undefined;
|
|
38
|
+
accept(notification: MspNotification): MuseStreamPart[];
|
|
39
|
+
private itemStartedEvent;
|
|
40
|
+
private itemDeltaEvent;
|
|
41
|
+
private itemCompletedEvent;
|
|
42
|
+
private tokenUsageEvent;
|
|
43
|
+
private turnCompleted;
|
|
44
|
+
private failAndFinish;
|
|
45
|
+
private finishPart;
|
|
46
|
+
}
|