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
package/dist/msp/host.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { MspError, EXPECTED_SCHEMA_FINGERPRINT, MuseHostStartError as SdkHostStartError, spawnMspConnection, } from "@muse-code/sdk";
|
|
2
|
+
import { MspCommandTimeoutError, MspHostClosedError, MspHostStartError, MspInitializeRejectedError, MspSchemaMismatchError, MspStartupTimeoutError, MspTransportError, } from "../errors.js";
|
|
3
|
+
import { MspClient } from "./client.js";
|
|
4
|
+
function defaultHandshake(options) {
|
|
5
|
+
const handshake = spawnMspConnection(options);
|
|
6
|
+
return {
|
|
7
|
+
async initialize(params) {
|
|
8
|
+
const spawned = await handshake.initialize(params);
|
|
9
|
+
return {
|
|
10
|
+
connection: spawned.connection,
|
|
11
|
+
initializeResult: { ...spawned.initializeResult },
|
|
12
|
+
exited: spawned.child.exit,
|
|
13
|
+
close: async () => spawned.close(),
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
close: async () => handshake.close(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function sanitizeExit(value) {
|
|
20
|
+
if (typeof value !== "object" || value === null)
|
|
21
|
+
return { kind: "unknown" };
|
|
22
|
+
const record = value;
|
|
23
|
+
const safe = {
|
|
24
|
+
kind: typeof record.kind === "string" ? record.kind : "unknown",
|
|
25
|
+
};
|
|
26
|
+
if (typeof record.exitCode === "number" || record.exitCode === null)
|
|
27
|
+
safe.exitCode = record.exitCode;
|
|
28
|
+
if (typeof record.exitSignal === "string" || record.exitSignal === null)
|
|
29
|
+
safe.exitSignal = record.exitSignal;
|
|
30
|
+
if (typeof record.retry === "string")
|
|
31
|
+
safe.retry = record.retry;
|
|
32
|
+
// Deliberately omit stderrTail, raw messages, and arbitrary child properties.
|
|
33
|
+
return safe;
|
|
34
|
+
}
|
|
35
|
+
function withTimeout(promise, timeoutMs) {
|
|
36
|
+
let timer;
|
|
37
|
+
return Promise.race([
|
|
38
|
+
promise,
|
|
39
|
+
new Promise((_resolve, reject) => {
|
|
40
|
+
timer = setTimeout(() => reject(new MspStartupTimeoutError(timeoutMs)), timeoutMs);
|
|
41
|
+
}),
|
|
42
|
+
]).finally(() => {
|
|
43
|
+
if (timer !== undefined)
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function closeQuietly(close) {
|
|
48
|
+
try {
|
|
49
|
+
await close();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Shutdown diagnostics may contain arbitrary CLI stderr; do not log them.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Owns one muse serve child and one MSP connection for one OpenCode session. */
|
|
56
|
+
export class MspHost {
|
|
57
|
+
runtime;
|
|
58
|
+
client;
|
|
59
|
+
initializeResult;
|
|
60
|
+
exited;
|
|
61
|
+
state = "open";
|
|
62
|
+
closePromise;
|
|
63
|
+
constructor(runtime, options) {
|
|
64
|
+
this.runtime = runtime;
|
|
65
|
+
this.initializeResult = runtime.initializeResult;
|
|
66
|
+
this.client = new MspClient(runtime.connection, {
|
|
67
|
+
commandTimeoutMs: options.commandTimeoutMs,
|
|
68
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
69
|
+
closeConnection: false,
|
|
70
|
+
onFatal: () => {
|
|
71
|
+
queueMicrotask(() => { void this.close().catch(() => { }); });
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
this.exited = runtime.exited
|
|
75
|
+
? runtime.exited.then((value) => {
|
|
76
|
+
this.state = "closed";
|
|
77
|
+
this.client.dispose();
|
|
78
|
+
return sanitizeExit(value);
|
|
79
|
+
}, () => {
|
|
80
|
+
this.state = "closed";
|
|
81
|
+
this.client.dispose();
|
|
82
|
+
return { kind: "unknown" };
|
|
83
|
+
})
|
|
84
|
+
: new Promise(() => { });
|
|
85
|
+
}
|
|
86
|
+
static async start(options = {}, dependencies = {}) {
|
|
87
|
+
const spawnOptions = {
|
|
88
|
+
command: options.museBin ?? "muse",
|
|
89
|
+
args: options.args,
|
|
90
|
+
cwd: options.cwd,
|
|
91
|
+
env: options.env,
|
|
92
|
+
// No stderr callback: the SDK's bounded diagnostic tail is never forwarded to plugin logs.
|
|
93
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs ?? 30_000,
|
|
94
|
+
};
|
|
95
|
+
let handshake;
|
|
96
|
+
try {
|
|
97
|
+
handshake = (dependencies.createHandshake ?? defaultHandshake)(spawnOptions);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new MspHostStartError("spawn-failed");
|
|
101
|
+
}
|
|
102
|
+
const initializeParams = {
|
|
103
|
+
clientInfo: options.clientInfo ?? { name: "opencode_muse", version: "0.1.0" },
|
|
104
|
+
...(options.capabilities ? { capabilities: options.capabilities } : {}),
|
|
105
|
+
};
|
|
106
|
+
const timeoutMs = options.startupTimeoutMs ?? 20_000;
|
|
107
|
+
const initializing = handshake.initialize(initializeParams);
|
|
108
|
+
try {
|
|
109
|
+
const runtime = await withTimeout(initializing, timeoutMs);
|
|
110
|
+
const schema = runtime.initializeResult.schema;
|
|
111
|
+
const servedFingerprint = typeof schema?.fingerprint === "string" ? schema.fingerprint : "missing";
|
|
112
|
+
if (servedFingerprint !== EXPECTED_SCHEMA_FINGERPRINT) {
|
|
113
|
+
await closeQuietly(runtime.close.bind(runtime));
|
|
114
|
+
throw new MspSchemaMismatchError(EXPECTED_SCHEMA_FINGERPRINT, servedFingerprint);
|
|
115
|
+
}
|
|
116
|
+
return new MspHost(runtime, options);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof MspSchemaMismatchError)
|
|
120
|
+
throw error;
|
|
121
|
+
if (error instanceof MspStartupTimeoutError) {
|
|
122
|
+
void initializing.then((runtime) => closeQuietly(runtime.close.bind(runtime))).catch(() => { });
|
|
123
|
+
await closeQuietly(handshake.close.bind(handshake));
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
await closeQuietly(handshake.close.bind(handshake));
|
|
127
|
+
if (error instanceof MspError)
|
|
128
|
+
throw new MspInitializeRejectedError(error.code, error.kind);
|
|
129
|
+
if (error instanceof SdkHostStartError) {
|
|
130
|
+
throw new MspHostStartError(error.failure.kind === "spawnFailed" ? "spawn-failed" : "host-exited");
|
|
131
|
+
}
|
|
132
|
+
throw new MspHostStartError("unknown");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
get isClosed() {
|
|
136
|
+
return this.state !== "open";
|
|
137
|
+
}
|
|
138
|
+
mintCommandId() {
|
|
139
|
+
if (this.state !== "open")
|
|
140
|
+
throw new MspHostClosedError();
|
|
141
|
+
return this.client.mintCommandId();
|
|
142
|
+
}
|
|
143
|
+
command(method, paramsWithCommandId) {
|
|
144
|
+
if (this.state !== "open")
|
|
145
|
+
return Promise.reject(new MspHostClosedError());
|
|
146
|
+
return this.client.command(method, paramsWithCommandId).catch((error) => {
|
|
147
|
+
if (error instanceof MspCommandTimeoutError)
|
|
148
|
+
void this.close().catch(() => { });
|
|
149
|
+
throw error;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
request(method, params) {
|
|
153
|
+
if (this.state !== "open")
|
|
154
|
+
return Promise.reject(new MspHostClosedError());
|
|
155
|
+
return this.client.request(method, params);
|
|
156
|
+
}
|
|
157
|
+
subscribe(sessionId, after, signal) {
|
|
158
|
+
if (this.state !== "open")
|
|
159
|
+
throw new MspHostClosedError();
|
|
160
|
+
return this.client.subscribe(sessionId, after, signal);
|
|
161
|
+
}
|
|
162
|
+
close() {
|
|
163
|
+
if (this.closePromise)
|
|
164
|
+
return this.closePromise;
|
|
165
|
+
this.state = "closing";
|
|
166
|
+
this.client.dispose();
|
|
167
|
+
this.closePromise = this.runtime.close().then(() => { this.state = "closed"; }, () => {
|
|
168
|
+
this.state = "closed";
|
|
169
|
+
throw new MspTransportError();
|
|
170
|
+
});
|
|
171
|
+
return this.closePromise;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { CatalogCache, type MuseCatalogSnapshot } from "./catalog.js";
|
|
3
|
+
import { MuseSessionManager } from "./session/manager.js";
|
|
4
|
+
export type MuseProviderContext = Pick<Plugin.Context, "provider" | "session" | "aisdk"> & {
|
|
5
|
+
location: Pick<Plugin.Context["location"], "directory">;
|
|
6
|
+
};
|
|
7
|
+
export interface ProviderPluginDependencies {
|
|
8
|
+
fetchCatalog?: (directory: string) => Promise<MuseCatalogSnapshot>;
|
|
9
|
+
createSessionManager?: () => MuseSessionManager;
|
|
10
|
+
}
|
|
11
|
+
export interface MuseProviderRuntime {
|
|
12
|
+
catalog: CatalogCache;
|
|
13
|
+
refreshTask: Promise<boolean>;
|
|
14
|
+
sessions: MuseSessionManager;
|
|
15
|
+
dispose(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Register the disabled offline row first, then refresh only from a live MSP provider catalog. */
|
|
18
|
+
export declare function createProviderPlugin(ctx: MuseProviderContext, moduleUrl: string, dependencies?: ProviderPluginDependencies): Promise<MuseProviderRuntime>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { MspHost } from "./msp/host.js";
|
|
2
|
+
import { CatalogCache, MUSE_PROVIDER_ID, } from "./catalog.js";
|
|
3
|
+
import { addRequestContextHeaders } from "./model/context.js";
|
|
4
|
+
import { createMuseLanguageModel } from "./model/language-model.js";
|
|
5
|
+
import { MuseSessionManager } from "./session/manager.js";
|
|
6
|
+
import { SessionRegistry } from "./session/registry.js";
|
|
7
|
+
import { WorkspaceLock } from "./session/workspace-lock.js";
|
|
8
|
+
import { MuseBridgeError } from "./errors.js";
|
|
9
|
+
import { makeCompactionDigest, makeLocalTitle } from "./model/meta.js";
|
|
10
|
+
import { fingerprintCompactionOutput } from "./session/fingerprint.js";
|
|
11
|
+
import { extractAllowedAgentContext } from "./model/agent-context.js";
|
|
12
|
+
function providerInfo(moduleUrl) {
|
|
13
|
+
return {
|
|
14
|
+
id: MUSE_PROVIDER_ID,
|
|
15
|
+
name: "Muse Code",
|
|
16
|
+
activation: "enabled",
|
|
17
|
+
package: `aisdk:${moduleUrl}`,
|
|
18
|
+
settings: {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function fetchMuseCatalog(directory) {
|
|
22
|
+
const host = await MspHost.start({
|
|
23
|
+
args: ["serve", "--provider", "meta", "--disable-shell", "--disable-write"],
|
|
24
|
+
cwd: directory,
|
|
25
|
+
startupTimeoutMs: 10_000,
|
|
26
|
+
requestTimeoutMs: 10_000,
|
|
27
|
+
shutdownTimeoutMs: 5_000,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
return await host.request("model/list");
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await host.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Register the disabled offline row first, then refresh only from a live MSP provider catalog. */
|
|
37
|
+
export async function createProviderPlugin(ctx, moduleUrl, dependencies = {}) {
|
|
38
|
+
const catalog = new CatalogCache();
|
|
39
|
+
const stateDirectory = SessionRegistry.resolveStateDirectory();
|
|
40
|
+
const sessions = dependencies.createSessionManager?.() ?? new MuseSessionManager({
|
|
41
|
+
stateDirectory,
|
|
42
|
+
registry: new SessionRegistry({ stateDirectory }),
|
|
43
|
+
workspaceLock: new WorkspaceLock({ stateDirectory }),
|
|
44
|
+
});
|
|
45
|
+
const registration = await ctx.provider.transform((providers) => {
|
|
46
|
+
providers.add({ info: providerInfo(moduleUrl), models: catalog.models });
|
|
47
|
+
});
|
|
48
|
+
let sessionHookRegistration;
|
|
49
|
+
let languageHookRegistration;
|
|
50
|
+
const localMetaRegistrations = [];
|
|
51
|
+
try {
|
|
52
|
+
sessionHookRegistration = await ctx.session.hook("model.request", async (request) => {
|
|
53
|
+
addRequestContextHeaders(request.headers, request, ctx.location.directory);
|
|
54
|
+
}, { providerID: MUSE_PROVIDER_ID });
|
|
55
|
+
localMetaRegistrations.push(await ctx.session.hook("context", async (input) => {
|
|
56
|
+
sessions.setContextForSession(input.sessionID, extractAllowedAgentContext(input.agent, input.system));
|
|
57
|
+
}, { providerID: MUSE_PROVIDER_ID }));
|
|
58
|
+
languageHookRegistration = await ctx.aisdk.hook("language", async (input) => {
|
|
59
|
+
if (input.model.providerID !== MUSE_PROVIDER_ID)
|
|
60
|
+
return;
|
|
61
|
+
const selection = catalog.selectionIndex.get(input.model.id);
|
|
62
|
+
if (!selection) {
|
|
63
|
+
throw new MuseBridgeError("MUSE_UNSUPPORTED_REQUEST", "Muse model selection is unavailable; refresh the provider catalog", false);
|
|
64
|
+
}
|
|
65
|
+
input.language = createMuseLanguageModel({
|
|
66
|
+
selectionIndex: catalog.selectionIndex,
|
|
67
|
+
resolveSession: (context) => sessions.resolveSession(context),
|
|
68
|
+
}, input.model.modelID);
|
|
69
|
+
}, { providerID: MUSE_PROVIDER_ID });
|
|
70
|
+
localMetaRegistrations.push(await ctx.session.hook("title", async (input) => {
|
|
71
|
+
input.result = makeLocalTitle(input.messages);
|
|
72
|
+
}, { providerID: MUSE_PROVIDER_ID }));
|
|
73
|
+
localMetaRegistrations.push(await ctx.session.hook("compaction", async (input) => {
|
|
74
|
+
const summary = makeCompactionDigest(input.messages, typeof input.options.maxOutputTokens === "number" ? input.options.maxOutputTokens : undefined);
|
|
75
|
+
await sessions.recordCompactionCandidate(input.sessionID, fingerprintCompactionOutput(summary));
|
|
76
|
+
input.result = { summary };
|
|
77
|
+
}, { providerID: MUSE_PROVIDER_ID }));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await sessions.close();
|
|
81
|
+
await registration.dispose();
|
|
82
|
+
// The session hook may have been registered before the language hook failed.
|
|
83
|
+
await sessionHookRegistration?.dispose();
|
|
84
|
+
await Promise.all(localMetaRegistrations.map((hook) => hook.dispose()));
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
let disposed = false;
|
|
88
|
+
const refreshTask = new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
|
|
89
|
+
if (disposed)
|
|
90
|
+
return false;
|
|
91
|
+
return catalog.refresh(() => (dependencies.fetchCatalog ?? fetchMuseCatalog)(ctx.location.directory), async () => {
|
|
92
|
+
if (!disposed)
|
|
93
|
+
await ctx.provider.reload();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
catalog,
|
|
98
|
+
refreshTask,
|
|
99
|
+
sessions,
|
|
100
|
+
async dispose() {
|
|
101
|
+
disposed = true;
|
|
102
|
+
await refreshTask;
|
|
103
|
+
await Promise.all([
|
|
104
|
+
registration.dispose(),
|
|
105
|
+
sessionHookRegistration?.dispose(),
|
|
106
|
+
languageHookRegistration?.dispose(),
|
|
107
|
+
...localMetaRegistrations.map((hook) => hook.dispose()),
|
|
108
|
+
sessions.close(),
|
|
109
|
+
]);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface VisiblePromptMessage {
|
|
2
|
+
role?: string;
|
|
3
|
+
content?: unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare function fingerprintTranscript(prompt: readonly VisiblePromptMessage[]): string[];
|
|
6
|
+
export declare function fingerprintCompactionOutput(summary: string): string;
|
|
7
|
+
export declare function promptContainsCompactionFingerprint(prompt: readonly VisiblePromptMessage[], candidateFingerprint: string): boolean;
|
|
8
|
+
export type TranscriptReconciliation = "same" | "compacted" | "diverged";
|
|
9
|
+
export declare function reconcileTranscript(previous: readonly string[], current: readonly string[], pendingCompactionFingerprints: readonly string[], prompt?: readonly VisiblePromptMessage[]): TranscriptReconciliation;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function normalizeText(text) {
|
|
3
|
+
return text.replace(/\r\n?/g, "\n").normalize("NFC");
|
|
4
|
+
}
|
|
5
|
+
function hash(value) {
|
|
6
|
+
return createHash("sha256").update(value).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function bytesForFileData(data) {
|
|
9
|
+
if (typeof data === "string")
|
|
10
|
+
return data;
|
|
11
|
+
if (data instanceof Uint8Array)
|
|
12
|
+
return data;
|
|
13
|
+
if (data instanceof URL)
|
|
14
|
+
return data.href;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function visibleParts(content) {
|
|
18
|
+
const parts = typeof content === "string" ? [{ type: "text", text: content }] : content;
|
|
19
|
+
if (!Array.isArray(parts))
|
|
20
|
+
return [];
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const rawPart of parts) {
|
|
23
|
+
if (typeof rawPart !== "object" || rawPart === null)
|
|
24
|
+
continue;
|
|
25
|
+
const part = rawPart;
|
|
26
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
27
|
+
result.push({ type: "text", digest: hash(normalizeText(part.text)) });
|
|
28
|
+
}
|
|
29
|
+
else if (part.type === "file" && typeof part.mediaType === "string") {
|
|
30
|
+
const bytes = bytesForFileData(part.data);
|
|
31
|
+
if (bytes !== undefined)
|
|
32
|
+
result.push({ type: "file", mediaType: part.mediaType, digest: hash(bytes) });
|
|
33
|
+
}
|
|
34
|
+
// Reasoning, tool calls/results, and system boilerplate are not visible
|
|
35
|
+
// user/assistant transcript lineage.
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
export function fingerprintTranscript(prompt) {
|
|
40
|
+
const fingerprints = [];
|
|
41
|
+
for (const message of prompt) {
|
|
42
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
43
|
+
continue;
|
|
44
|
+
const parts = visibleParts(message.content);
|
|
45
|
+
if (parts.length === 0)
|
|
46
|
+
continue;
|
|
47
|
+
fingerprints.push(hash(JSON.stringify({ role: message.role, parts })));
|
|
48
|
+
}
|
|
49
|
+
return fingerprints;
|
|
50
|
+
}
|
|
51
|
+
export function fingerprintCompactionOutput(summary) {
|
|
52
|
+
return hash(normalizeText(summary));
|
|
53
|
+
}
|
|
54
|
+
export function promptContainsCompactionFingerprint(prompt, candidateFingerprint) {
|
|
55
|
+
if (!/^[a-f0-9]{64}$/.test(candidateFingerprint))
|
|
56
|
+
return false;
|
|
57
|
+
for (const message of prompt) {
|
|
58
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
59
|
+
continue;
|
|
60
|
+
const parts = typeof message.content === "string"
|
|
61
|
+
? [{ type: "text", text: message.content }]
|
|
62
|
+
: message.content;
|
|
63
|
+
if (!Array.isArray(parts))
|
|
64
|
+
continue;
|
|
65
|
+
for (const rawPart of parts) {
|
|
66
|
+
if (typeof rawPart !== "object" || rawPart === null)
|
|
67
|
+
continue;
|
|
68
|
+
const part = rawPart;
|
|
69
|
+
if (part.type === "text" && typeof part.text === "string"
|
|
70
|
+
&& fingerprintCompactionOutput(part.text) === candidateFingerprint)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
export function reconcileTranscript(previous, current, pendingCompactionFingerprints, prompt) {
|
|
77
|
+
if (previous.length <= current.length && previous.every((fingerprint, index) => current[index] === fingerprint))
|
|
78
|
+
return "same";
|
|
79
|
+
if (prompt && pendingCompactionFingerprints.some((candidate) => promptContainsCompactionFingerprint(prompt, candidate))) {
|
|
80
|
+
return "compacted";
|
|
81
|
+
}
|
|
82
|
+
return "diverged";
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MspHandshake } from "@muse-code/sdk";
|
|
2
|
+
import type { RequestContext } from "../types.js";
|
|
3
|
+
import { MspHost } from "../msp/host.js";
|
|
4
|
+
import type { SessionRegistry } from "./registry.js";
|
|
5
|
+
import type { WorkspaceLock } from "./workspace-lock.js";
|
|
6
|
+
import type { MuseTurnSession } from "../model/language-model.js";
|
|
7
|
+
import type { AgentContextResult } from "../model/agent-context.js";
|
|
8
|
+
export interface MuseSessionHost extends Pick<MspHost, "command" | "request" | "subscribe" | "mintCommandId" | "close"> {
|
|
9
|
+
initializeResult: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface MuseSessionManagerOptions {
|
|
12
|
+
stateDirectory?: string;
|
|
13
|
+
registry: SessionRegistry;
|
|
14
|
+
workspaceLock: WorkspaceLock;
|
|
15
|
+
museBin?: string;
|
|
16
|
+
createHost?(options: {
|
|
17
|
+
museBin?: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
args: readonly string[];
|
|
20
|
+
capabilities?: Parameters<MspHandshake["initialize"]>[0]["capabilities"];
|
|
21
|
+
}): Promise<MuseSessionHost>;
|
|
22
|
+
videoChunkBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function workspaceFingerprint(directory: string): string;
|
|
25
|
+
export declare class MuseSessionManager {
|
|
26
|
+
private readonly options;
|
|
27
|
+
private readonly bindings;
|
|
28
|
+
private readonly contextBySession;
|
|
29
|
+
private readonly createHost;
|
|
30
|
+
private closed;
|
|
31
|
+
constructor(options: MuseSessionManagerOptions);
|
|
32
|
+
setContextForSession(openCodeSessionId: string, context: AgentContextResult): void;
|
|
33
|
+
resolveSession(context: RequestContext): Promise<MuseTurnSession>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
recordCompactionCandidate(openCodeSessionId: string, fingerprint: string): Promise<void>;
|
|
36
|
+
private applySelectionIfChanged;
|
|
37
|
+
private readonly bindingSessionIDs;
|
|
38
|
+
private openCodeSessionIdFor;
|
|
39
|
+
private persistBinding;
|
|
40
|
+
}
|