diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 35fec0c2769ac7372925b5ec71f63e0ca1873993..08307ad65c2ac6c5c048b57aa9ae85d2feb690b1 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -70,6 +70,7 @@
"@t3tools/mobile-review-diff-native": "file:./modules/t3-review-diff",
"@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
"@t3tools/provider-core": "workspace:*",
+ "@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
"@t3tools/shared": "workspace:*",
"@tabler/icons-react-native": "^3.44.0",
diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx
index 1675b15461d06033532e81a00fd6bd95e593ee94..0645e673981817cdd1b4eae9340d43c9f9949b45 100644
--- a/apps/mobile/src/components/ProviderIcon.tsx
+++ b/apps/mobile/src/components/ProviderIcon.tsx
@@ -120,18 +120,6 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}
- if (props.provider === "muse") {
- return (
-
- );
- }
-
const packageIcon = getProviderClient(props.provider)?.icon;
if (packageIcon) {
const themeFill = (fill: { readonly light: string; readonly dark: string }) =>
diff --git a/apps/mobile/src/lib/providerClients.ts b/apps/mobile/src/lib/providerClients.ts
index c8c1c05c01580e8e9e06b5750a7781458d9ffdee..2d3ec1224992afbee93798b67c5865208f17a392 100644
--- a/apps/mobile/src/lib/providerClients.ts
+++ b/apps/mobile/src/lib/providerClients.ts
@@ -1,9 +1,10 @@
import { isProviderDriverKind } from "@t3tools/contracts";
import { makeProviderClientRegistry } from "@t3tools/provider-core/client";
+import { museClient } from "@t3tools/provider-muse/client";
import { piClient } from "@t3tools/provider-pi/client";
/** The provider client definitions this mobile build ships. */
-const providerClients = makeProviderClientRegistry([piClient]);
+const providerClients = makeProviderClientRegistry([museClient, piClient]);
/** The client definition for a driver kind, or `undefined` for drivers drawn by hand. */
export function getProviderClient(driver: string | null | undefined) {
diff --git a/apps/server/package.json b/apps/server/package.json
index 7672fbf534e3e8fb9765a8acac362e9da04cc52b..bb776149e4cd55099e572c832342ef2642029ab3 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -42,6 +42,7 @@
"@opencode/protocol": "2.0.23",
"@opencode/schema": "2.0.23",
"@t3tools/provider-core": "workspace:*",
+ "@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
"diff": "8.0.3",
"effect": "catalog:",
diff --git a/apps/server/scripts/record-muse-msp-replay-fixture.ts b/apps/server/scripts/record-muse-msp-replay-fixture.ts
index dde469487be077407738b0de633fab8cfef08c71..a3cbfc6f9d14cb2f4c060f50fd341c963d28fa2b 100644
--- a/apps/server/scripts/record-muse-msp-replay-fixture.ts
+++ b/apps/server/scripts/record-muse-msp-replay-fixture.ts
@@ -33,8 +33,11 @@ import {
MuseOrchestratorReplayHarness,
} from "../src/orchestration-v2/Adapters/MuseAdapterV2.testkit.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import { parseMuseVersion } from "../src/provider/museMaintenance.ts";
-import { makeMuseEnvironment, museServeArgs } from "../src/provider/museSdk.ts";
+import {
+ makeMuseEnvironment,
+ museServeArgs,
+ parseMuseVersion,
+} from "@t3tools/provider-muse/testing";
import { provideDeterministicTestRuntime } from "../src/orchestration-v2/testkit/DeterministicRuntime.ts";
import { ORCHESTRATOR_REPLAY_FIXTURES } from "../src/orchestration-v2/testkit/fixtures/index.ts";
import { materializeFixtureInput } from "../src/orchestration-v2/testkit/fixtures/shared.ts";
diff --git a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.test.ts
deleted file mode 100644
index 77e12726a1834a02ae5c79184af93b84dc758f40..0000000000000000000000000000000000000000
--- a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.test.ts
+++ /dev/null
@@ -1,1108 +0,0 @@
-import { assert, describe, it } from "@effect/vitest";
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { MspError } from "@muse-code/sdk";
-import {
- MUSE_DEFAULT_MODEL,
- EnvironmentId,
- MessageId,
- MuseSettings,
- NodeId,
- ProjectId,
- ProviderInstanceId,
- ProviderDriverKind,
- ProviderSessionId,
- ProviderThreadId,
- ProviderTurnId,
- RunAttemptId,
- RunId,
- ThreadId,
- type OrchestrationV2AppThread,
- type OrchestrationV2ProviderThread,
-} from "@t3tools/contracts";
-import * as DateTime from "effect/DateTime";
-import * as Deferred from "effect/Deferred";
-import * as Effect from "effect/Effect";
-import * as Option from "effect/Option";
-import type * as Exit from "effect/Exit";
-import * as FileSystem from "effect/FileSystem";
-import * as Fiber from "effect/Fiber";
-import * as Layer from "effect/Layer";
-import * as Queue from "effect/Queue";
-import * as Schema from "effect/Schema";
-import * as Stream from "effect/Stream";
-
-import * as ServerConfig from "../../config.ts";
-import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
-import type { MuseItem } from "../../provider/museProtocol.ts";
-import type { MuseSdkHost } from "../../provider/museSdk.ts";
-import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import {
- ProviderAdapterV2RuntimePolicy,
- type ProviderAdapterV2Event,
- type ProviderAdapterV2TurnInput,
-} from "@t3tools/provider-core/server/ProviderAdapter";
-import type { ProviderContinuationRequest } from "@t3tools/provider-core/server/continuationRequests";
-import { makeMuseAdapterV2, type MuseAdapterV2Options } from "./MuseAdapterV2.ts";
-
-const testLayer = Layer.mergeAll(
- NodeServices.layer,
- IdAllocator.layer,
- ServerConfig.layerTest(process.cwd(), { prefix: "t3-muse-v2-adapter-" }).pipe(
- Layer.provide(NodeServices.layer),
- ),
-);
-const MUSE_PROVIDER = ProviderDriverKind.make("muse");
-const INSTANCE_ID = ProviderInstanceId.make("muse_work");
-const THREAD_ID = ThreadId.make("thread-muse-test");
-const MODEL = "muse-spark-1.3-contributor";
-const museSettings = Schema.decodeSync(MuseSettings)({ enabled: true });
-const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
- runtimeMode: "full-access",
- interactionMode: "default",
- cwd: null,
-});
-const modelSelection = (instanceId = INSTANCE_ID) => ({
- instanceId,
- model: MODEL,
- options: [{ id: "reasoningEffort", value: "max" }],
-});
-
-interface NativeCall {
- readonly method: string;
- readonly params: Record;
- readonly commandId?: string;
-}
-
-function pendingPromise() {
- let resolve!: (value: A) => void;
- let reject!: (reason: unknown) => void;
- const promise = new Promise((done, fail) => {
- resolve = done;
- reject = fail;
- });
- return { promise, resolve, reject };
-}
-
-/** SDK-shaped transport fake: registration deliberately returns void, as the SDK does. */
-const makeFakeMuse = Effect.fnUntraced(function* (idPrefix = "native") {
- const requests = yield* Queue.unbounded();
- const calls: NativeCall[] = [];
- const closed = pendingPromise();
- const exited = pendingPromise>();
- let notify: Parameters[0] = () => {};
- let sequence = 0;
- let sessionId = "";
- let closeCount = 0;
- let closeGate: Promise | undefined;
- let history: MuseItem[] = [];
- const responses = new Map<
- string,
- Array> | Record | Error>
- >();
- const respond = async (call: NativeCall): Promise> => {
- calls.push(call);
- Queue.offerUnsafe(requests, call);
- if (["session/start", "session/resume", "session/read"].includes(call.method)) {
- sessionId = String(call.params.sessionId);
- }
- const queued = responses.get(call.method)?.shift();
- if (queued instanceof Error) throw queued;
- if (queued) return queued;
- switch (call.method) {
- case "session/start":
- case "session/resume":
- case "session/read":
- sessionId = String(call.params.sessionId);
- return {
- session: { sessionId, modelId: MODEL, activeTurnId: null },
- history: { items: history },
- };
- case "turn/start":
- return { turnId: call.commandId };
- case "turn/steer":
- return { turnId: call.params.expectedTurnId };
- case "session/compact":
- return { status: "accepted" };
- default:
- return {};
- }
- };
- const host: MuseSdkHost = {
- connection: {
- mintCommandId: () => `${idPrefix}-${++sequence}`,
- command: (method, params, options) =>
- respond({
- method,
- params,
- ...(options?.commandId ? { commandId: options.commandId } : {}),
- }),
- request: (method, params = {}) => respond({ method, params }),
- onNotification: (handler) => {
- notify = handler;
- },
- onProtocolError: () => {},
- onServerRequest: () => {},
- closed: closed.promise,
- },
- initializeResult: { grantedCapabilities: [] },
- exited: exited.promise,
- close: async () => {
- closeCount += 1;
- await closeGate;
- closed.resolve();
- exited.resolve({ code: 0, signal: null });
- },
- };
- return {
- host,
- calls,
- closeCount: () => closeCount,
- holdClose: (gate: Promise) => {
- closeGate = gate;
- },
- disconnect: () =>
- Effect.sync(() => {
- closed.resolve();
- exited.resolve({ code: 1, signal: null });
- }),
- setHistory: (items: MuseItem[]) => {
- history = items;
- },
- queueResponse: (
- method: string,
- result: Record | Promise> | Error,
- ) => {
- responses.set(method, [...(responses.get(method) ?? []), result]);
- },
- emit: (method: string, params: Record) =>
- Effect.sync(() => {
- notify({ jsonrpc: "2.0", method, params: { sessionId, ...params } });
- }),
- takeCall: Effect.fnUntraced(function* (method: string) {
- while (true) {
- const call = yield* Queue.take(requests);
- if (call.method === method) return call;
- }
- }),
- };
-});
-
-const makeHarness = Effect.fnUntraced(function* (
- fake: Effect.Success>,
- instanceId = INSTANCE_ID,
- initialNativeThreadId?: string,
- replacement?: Effect.Success>,
- existingProviderThread?: OrchestrationV2ProviderThread,
- policy = runtimePolicy,
- overrides: Pick<
- MuseAdapterV2Options,
- "createHost" | "nativeEventLogger" | "modelCatalog" | "continuationRequests"
- > = {},
-) {
- let hostCount = 0;
- const adapter = makeMuseAdapterV2({
- instanceId,
- settings: museSettings,
- environment: { PATH: "/fake/bin" },
- idAllocator: yield* IdAllocator.IdAllocatorV2,
- serverConfig: yield* ServerConfig.ServerConfig,
- fileSystem: yield* FileSystem.FileSystem,
- createHost: async () => (hostCount++ === 0 ? fake.host : (replacement ?? fake).host),
- ...overrides,
- });
- const runtime = yield* adapter.openSession({
- threadId: THREAD_ID,
- providerSessionId: ProviderSessionId.make(`session-${instanceId}`),
- modelSelection: modelSelection(instanceId),
- runtimePolicy: policy,
- ...(initialNativeThreadId ? { initialNativeThreadId } : {}),
- });
- const emitted = yield* Queue.unbounded();
- const allEvents: ProviderAdapterV2Event[] = [];
- const eventsEnded = yield* Deferred.make>>();
- yield* runtime.events.pipe(
- Stream.runForEach((event) =>
- Effect.gen(function* () {
- allEvents.push(event);
- yield* Queue.offer(emitted, event);
- }),
- ),
- Effect.exit,
- Effect.flatMap((exit) => Deferred.succeed(eventsEnded, exit)),
- Effect.forkScoped,
- );
- const providerThread = yield* runtime.ensureThread({
- threadId: THREAD_ID,
- modelSelection: modelSelection(instanceId),
- runtimePolicy: policy,
- ...(existingProviderThread ? { existingProviderThread } : {}),
- });
- const takeEvent = Effect.fnUntraced(function* (
- type: T,
- predicate: (event: Extract) => boolean = () => true,
- ) {
- while (true) {
- const event = yield* Queue.take(emitted);
- if (event.type === type && predicate(event as Extract)) {
- return event as Extract;
- }
- }
- });
- return { adapter, runtime, providerThread, takeEvent, allEvents, policy, eventsEnded };
-});
-
-const preallocatedProviderThread = Effect.fnUntraced(function* () {
- const now = yield* DateTime.now;
- return {
- id: ProviderThreadId.make("preallocated-muse-thread"),
- driver: MUSE_PROVIDER,
- providerInstanceId: INSTANCE_ID,
- providerSessionId: ProviderSessionId.make(`session-${INSTANCE_ID}`),
- appThreadId: THREAD_ID,
- ownerNodeId: null,
- nativeThreadRef: null,
- nativeConversationHeadRef: null,
- status: "idle",
- firstRunOrdinal: null,
- lastRunOrdinal: null,
- handoffIds: [],
- forkedFrom: null,
- createdAt: now,
- updatedAt: now,
- } satisfies OrchestrationV2ProviderThread;
-});
-
-const turnInput = Effect.fnUntraced(function* (
- providerThread: OrchestrationV2ProviderThread,
- runOrdinal = 1,
- policy = runtimePolicy,
-): Effect.fn.Return {
- const now = yield* DateTime.now;
- const threadId = providerThread.appThreadId ?? THREAD_ID;
- const selection = modelSelection(providerThread.providerInstanceId);
- const appThread: OrchestrationV2AppThread = {
- id: threadId,
- projectId: ProjectId.make("project-muse-test"),
- title: "Muse test",
- createdBy: "user",
- creationSource: "web",
- providerInstanceId: providerThread.providerInstanceId,
- modelSelection: selection,
- runtimeMode: policy.runtimeMode,
- interactionMode: policy.interactionMode,
- branch: null,
- worktreePath: null,
- activeProviderThreadId: providerThread.id,
- lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
- forkedFrom: null,
- createdAt: now,
- updatedAt: now,
- archivedAt: null,
- settledOverride: null,
- settledAt: null,
- lastVisitedAt: null,
- deletedAt: null,
- };
- return {
- appThread,
- threadId,
- runId: RunId.make(`run-${runOrdinal}`),
- runOrdinal,
- providerTurnOrdinal: runOrdinal,
- attemptId: RunAttemptId.make(`attempt-${runOrdinal}`),
- rootNodeId: NodeId.make(`root-${runOrdinal}`),
- providerThread,
- message: {
- messageId: MessageId.make(`message-${runOrdinal}`),
- text: "Hello Muse",
- attachments: [],
- createdBy: "user",
- creationSource: "web",
- },
- modelSelection: selection,
- runtimePolicy: policy,
- };
-});
-
-const startConversation = Effect.fnUntraced(function* (
- harness: Effect.Success>,
- fake: Effect.Success>,
- ordinal = 1,
-) {
- yield* harness.runtime.startTurn(
- yield* turnInput(harness.providerThread, ordinal, harness.policy),
- );
- const call = yield* fake.takeCall("turn/start");
- const event = yield* harness.takeEvent(
- "provider_turn.updated",
- (event) => event.providerTurn.status === "running",
- );
- return { nativeId: call.commandId!, providerTurn: event.providerTurn };
-});
-
-const approval = (turnId: string) => ({
- approvalId: "approval-1",
- turnId,
- currentRequirementId: { approvalId: "approval-1", sourceIndex: 0 },
- availableChoices: [
- { choiceId: "once", label: "Allow once", decision: "approved", scope: "once" },
- { choiceId: "deny", label: "Deny", decision: "denied", scope: "once" },
- ],
- subject: { kind: "shell", command: "touch result.txt" },
-});
-
-describe("MuseAdapterV2", () => {
- it.effect("connects the thread's MCP credential on native start and resume", () =>
- Effect.gen(function* () {
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID)),
- );
- McpProviderSession.setMcpProviderSession({
- environmentId: EnvironmentId.make("test-environment"),
- threadId: THREAD_ID,
- providerSessionId: "test-session",
- providerInstanceId: INSTANCE_ID,
- endpoint: "http://127.0.0.1:43210/mcp",
- authorizationHeader: "Bearer thread-scoped-test-token",
- browserToolsAvailable: true,
- });
- for (const nativeId of [undefined, "saved-native-session"]) {
- const fake = yield* makeFakeMuse();
- fake.host.initializeResult.grantedCapabilities.push("sessionMcp");
- yield* makeHarness(fake, INSTANCE_ID, nativeId);
- const call = yield* fake.takeCall(nativeId ? "session/resume" : "session/start");
- assert.deepStrictEqual(call.params.config, {
- mcpServers: {
- "t3-code": {
- transport: "streamableHttp",
- mode: "optional",
- url: "http://127.0.0.1:43210/mcp",
- headers: { Authorization: "Bearer thread-scoped-test-token" },
- },
- },
- });
- }
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("rejects a host without session MCP support before opening a native session", () =>
- Effect.gen(function* () {
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID)),
- );
- McpProviderSession.setMcpProviderSession({
- environmentId: EnvironmentId.make("test-environment"),
- threadId: THREAD_ID,
- providerSessionId: "test-session",
- providerInstanceId: INSTANCE_ID,
- endpoint: "http://127.0.0.1:43210/mcp",
- authorizationHeader: "Bearer thread-scoped-test-token",
- browserToolsAvailable: false,
- });
- const fake = yield* makeFakeMuse();
- const outcome = yield* makeHarness(fake).pipe(Effect.result);
- assert.strictEqual(outcome._tag, "Failure");
- assert.isFalse(fake.calls.some((call) => call.method === "session/start"));
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect(
- "starts fresh after recovery clears a native ref instead of reusing the startup hint",
- () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const cleared = yield* preallocatedProviderThread();
- const harness = yield* makeHarness(
- fake,
- INSTANCE_ID,
- "failed-old-session",
- undefined,
- cleared,
- );
- const started = yield* fake.takeCall("session/start");
- assert.isFalse(fake.calls.some((call) => call.method === "session/resume"));
- assert.notStrictEqual(started.params.sessionId, "failed-old-session");
- assert.strictEqual(harness.providerThread.id, cleared.id);
- yield* startConversation(harness, fake);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("retries a missing native session with a cleared ref on the same healthy host", () =>
- Effect.gen(function* () {
- for (const kind of ["sessionNotFound", "notFound"] as const) {
- const fake = yield* makeFakeMuse();
- const allocated = yield* preallocatedProviderThread();
- const adapter = makeMuseAdapterV2({
- instanceId: INSTANCE_ID,
- settings: museSettings,
- environment: { PATH: "/fake/bin" },
- idAllocator: yield* IdAllocator.IdAllocatorV2,
- serverConfig: yield* ServerConfig.ServerConfig,
- fileSystem: yield* FileSystem.FileSystem,
- createHost: async () => fake.host,
- });
- const runtime = yield* adapter.openSession({
- threadId: THREAD_ID,
- providerSessionId: ProviderSessionId.make(`missing-${kind}`),
- modelSelection: modelSelection(),
- runtimePolicy,
- initialNativeThreadId: "missing-session",
- });
- fake.queueResponse(
- "session/resume",
- new MspError({ code: -32000, message: "Saved session is absent", data: { kind } }),
- );
- const failed = yield* runtime
- .ensureThread({
- threadId: THREAD_ID,
- modelSelection: modelSelection(),
- runtimePolicy,
- existingProviderThread: {
- ...allocated,
- nativeThreadRef: {
- driver: MUSE_PROVIDER,
- nativeId: "missing-session",
- strength: "strong",
- },
- },
- })
- .pipe(Effect.exit);
- assert.strictEqual(failed._tag, "Failure");
- assert.strictEqual(fake.closeCount(), 0);
- const fresh = yield* runtime.ensureThread({
- threadId: THREAD_ID,
- modelSelection: modelSelection(),
- runtimePolicy,
- existingProviderThread: allocated,
- });
- assert.strictEqual(fresh.id, allocated.id);
- assert.notStrictEqual(fresh.nativeThreadRef?.nativeId, "missing-session");
- assert.strictEqual(fake.closeCount(), 0);
- yield* runtime.startTurn(yield* turnInput(fresh));
- const admitted = yield* fake.takeCall("turn/start");
- assert.strictEqual(admitted.params.reasoningEffort, "max");
- yield* fake.emit("turn/completed", { turnId: admitted.commandId, terminal: "completed" });
- yield* Stream.runDrain(
- runtime.events.pipe(Stream.takeUntil((event) => event.type === "turn.terminal")),
- );
- }
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("rejects cross-instance ensure requests without closing the current valid host", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const rejected = yield* harness.runtime
- .ensureThread({
- threadId: THREAD_ID,
- modelSelection: modelSelection(),
- runtimePolicy,
- existingProviderThread: {
- ...harness.providerThread,
- providerInstanceId: ProviderInstanceId.make("other-account"),
- },
- })
- .pipe(Effect.exit);
- assert.strictEqual(rejected._tag, "Failure");
- assert.strictEqual(fake.closeCount(), 0);
- yield* startConversation(harness, fake);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("skips an unreadable usage notification without ending the turn", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("session/tokenUsage", { turnId: nativeId, usage: "not usage" });
- yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "completed");
- assert.strictEqual(fake.closeCount(), 0);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("moves a resumed session on another model back to the catalog default", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- // The saved session last ran MODEL; the catalog's default is another model.
- const harness = yield* makeHarness(
- fake,
- INSTANCE_ID,
- "saved-session",
- undefined,
- undefined,
- runtimePolicy,
- {
- modelCatalog: Effect.succeed([
- {
- slug: "account-default",
- name: "Default",
- isCustom: false,
- isDefault: true,
- capabilities: null,
- },
- { slug: MODEL, name: "Other", isCustom: false, isDefault: false, capabilities: null },
- ]),
- },
- );
- const input = yield* turnInput(harness.providerThread, 1);
- const selection = { ...input.modelSelection, model: MUSE_DEFAULT_MODEL };
- yield* harness.runtime.startTurn({
- ...input,
- modelSelection: selection,
- appThread: { ...input.appThread, modelSelection: selection },
- });
- const setModel = yield* fake.takeCall("session/setModel");
- assert.deepStrictEqual(setModel.params.model, {
- modelId: "account-default",
- providerId: "meta",
- });
- yield* fake.takeCall("turn/start");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("settles partial messages and pending approvals before a provider failure", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("item/started", {
- turnId: nativeId,
- item: {
- itemId: "partial",
- turnId: nativeId,
- kind: "agentMessage",
- status: "inProgress",
- revision: 1,
- text: "Partial answer",
- },
- });
- yield* fake.emit("approval/requested", approval(nativeId));
- yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- yield* fake.emit("turn/completed", {
- turnId: nativeId,
- terminal: "failed",
- error: { message: "Provider rejected this turn" },
- });
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "failed");
- assert.strictEqual(terminal.threadDisposition, "reusable");
- assert.isNotNull(terminal.failure);
- assert.isTrue(
- harness.allEvents.some(
- (event) =>
- event.type === "message.updated" &&
- !event.message.streaming &&
- event.message.text === "Partial answer",
- ),
- );
- assert.isTrue(
- harness.allEvents.some(
- (event) =>
- event.type === "runtime_request.updated" && event.runtimeRequest.status === "cancelled",
- ),
- );
- yield* startConversation(harness, fake, 2);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("closes its SDK host exactly once when its session scope ends", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- yield* makeHarness(fake).pipe(Effect.scoped);
- assert.strictEqual(fake.closeCount(), 1);
- }).pipe(Effect.provide(testLayer)),
- );
-
- it.effect(
- "closes a newly opened host when native session registration returns another identity",
- () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- fake.queueResponse("session/start", {
- session: { sessionId: "unexpected-session", modelId: MODEL, activeTurnId: null },
- history: { items: [] },
- });
- const result = yield* makeHarness(fake).pipe(Effect.exit);
- assert.strictEqual(result._tag, "Failure");
- // A failed host closes in the background so the failure shows at once.
- yield* Effect.promise(() => fake.host.exited);
- assert.strictEqual(fake.closeCount(), 1);
- assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 0);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("settles an active turn once when both the connection and host exit unexpectedly", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("approval/requested", approval(nativeId));
- yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- yield* fake.disconnect();
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "failed");
- assert.strictEqual(terminal.threadDisposition, "broken");
- assert.isTrue(
- harness.allEvents.some(
- (event) =>
- event.type === "runtime_request.updated" && event.runtimeRequest.status === "cancelled",
- ),
- );
- const result = yield* harness.runtime
- .startTurn(yield* turnInput(harness.providerThread, 2))
- .pipe(Effect.exit);
- assert.strictEqual(result._tag, "Failure");
- assert.strictEqual(
- harness.allEvents.filter((event) => event.type === "turn.terminal").length,
- 1,
- );
- assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 1);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("turns a delivery gap into a broken terminal and rejects subsequent work", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- yield* startConversation(harness, fake);
- yield* fake.emit("view/gap", {});
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "failed");
- assert.strictEqual(terminal.threadDisposition, "broken");
- yield* Effect.promise(() => fake.host.exited);
- assert.strictEqual(fake.closeCount(), 1);
- yield* Deferred.await(harness.eventsEnded);
- const result = yield* harness.runtime
- .startTurn(yield* turnInput(harness.providerThread, 2))
- .pipe(Effect.exit);
- assert.strictEqual(result._tag, "Failure");
- assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 1);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("uses the latest native approval requirement and waits for its resolution event", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("approval/requested", approval(nativeId));
- const first = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- yield* fake.emit("approval/updated", {
- ...approval(nativeId),
- currentRequirementId: { approvalId: "approval-1", sourceIndex: 1 },
- });
- const refreshed = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- assert.strictEqual(refreshed.runtimeRequest.id, first.runtimeRequest.id);
- yield* harness.runtime.respondToRuntimeRequest({
- requestId: first.runtimeRequest.id,
- decision: "accept",
- });
- const decision = yield* fake.takeCall("approval/decide");
- assert.deepStrictEqual(decision.params.requirementId, {
- approvalId: "approval-1",
- sourceIndex: 1,
- });
- assert.strictEqual(decision.params.choiceId, "once");
- assert.isFalse(
- harness.allEvents.some(
- (event) =>
- event.type === "runtime_request.updated" && event.runtimeRequest.status === "resolved",
- ),
- );
- yield* fake.emit("approval/resolved", { approvalId: "approval-1", turnId: nativeId });
- const resolved = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "resolved",
- );
- assert.strictEqual(resolved.runtimeRequest.id, first.runtimeRequest.id);
- const repeated = yield* harness.runtime
- .respondToRuntimeRequest({ requestId: first.runtimeRequest.id, decision: "accept" })
- .pipe(Effect.exit);
- assert.strictEqual(repeated._tag, "Failure");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("keeps the user's decision when Muse settles before acknowledging it", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("approval/requested", approval(nativeId));
- const pending = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- const acknowledgement = pendingPromise>();
- fake.queueResponse("approval/decide", acknowledgement.promise);
- const responding = yield* harness.runtime
- .respondToRuntimeRequest({ requestId: pending.runtimeRequest.id, decision: "decline" })
- .pipe(Effect.forkScoped);
- yield* fake.takeCall("approval/decide");
- // Muse's native decision for this choice maps to decline too; "cancel" proves it was ours.
- yield* fake.emit("approval/resolved", {
- approvalId: "approval-1",
- turnId: nativeId,
- decision: "abort",
- });
- const resolved = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "resolved",
- );
- assert.strictEqual(resolved.runtimeRequest.decision, "decline");
- acknowledgement.resolve({});
- yield* Fiber.join(responding);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("lets a user turn take a held Muse report turn, approvals included", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const offers: Array = [];
- const harness = yield* makeHarness(
- fake,
- INSTANCE_ID,
- undefined,
- undefined,
- undefined,
- runtimePolicy,
- { continuationRequests: { offer: (request) => Effect.sync(() => offers.push(request)) } },
- );
- // A finished workflow's report turn starts while T3 runs nothing: it is held.
- yield* fake.emit("turn/started", { turnId: "report-1" });
- yield* Effect.yieldNow;
- assert.lengthOf(offers, 1);
- // The user sends a message first; Muse runs the report before it.
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("approval/requested", approval("report-1"));
- const asked = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "pending",
- );
- yield* harness.runtime.respondToRuntimeRequest({
- requestId: asked.runtimeRequest.id,
- decision: "accept",
- });
- yield* fake.takeCall("approval/decide");
- yield* fake.emit("approval/resolved", { approvalId: "approval-1", turnId: "report-1" });
- yield* fake.emit("turn/completed", { turnId: "report-1", terminal: "completed" });
- yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "completed");
- // The user turn took it, so the continuation is not dispatched.
- const dispatched = yield* offers[0]!.dispatchIfCurrent!(Effect.succeed("run"));
- assert.isTrue(Option.isNone(dispatched));
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("approves a workflow child's approval in full access after its turn ended", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
- yield* harness.takeEvent("turn.terminal");
- // Muse runs workflow children without the session's allowAll mode.
- const childApproval = { ...approval("child-run-1"), subagentOrigin: { subagentId: "a7" } };
- yield* fake.emit("approval/requested", childApproval);
- const asked = yield* harness.takeEvent("runtime_request.updated");
- assert.strictEqual(asked.runtimeRequest.status, "pending");
- assert.isNull(asked.runtimeRequest.providerTurnId);
- const decision = yield* fake.takeCall("approval/decide");
- assert.strictEqual(decision.params.choiceId, "once");
- yield* fake.emit("approval/resolved", childApproval);
- const resolved = yield* harness.takeEvent("runtime_request.updated");
- assert.strictEqual(resolved.runtimeRequest.status, "resolved");
- assert.strictEqual(resolved.runtimeRequest.decision, "accept");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("keeps a workflow child's approval pending for the user past the turn's end", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const policy = ProviderAdapterV2RuntimePolicy.make({
- ...runtimePolicy,
- runtimeMode: "approval-required",
- });
- const harness = yield* makeHarness(
- fake,
- INSTANCE_ID,
- undefined,
- undefined,
- undefined,
- policy,
- );
- const { nativeId } = yield* startConversation(harness, fake);
- const childApproval = { ...approval("child-run-1"), subagentOrigin: { subagentId: "a7" } };
- yield* fake.emit("approval/requested", childApproval);
- const asked = yield* harness.takeEvent("runtime_request.updated");
- assert.isNull(asked.runtimeRequest.providerTurnId);
- yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
- yield* harness.takeEvent("turn.terminal");
- assert.isFalse(
- harness.allEvents.some(
- (event) =>
- event.type === "runtime_request.updated" && event.runtimeRequest.status !== "pending",
- ),
- );
- yield* harness.runtime.respondToRuntimeRequest({
- requestId: asked.runtimeRequest.id,
- decision: "decline",
- });
- const decision = yield* fake.takeCall("approval/decide");
- assert.strictEqual(decision.params.choiceId, "deny");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("validates structured answers and preserves selected labels with a custom note", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- yield* fake.emit("userInput/requested", {
- userInputId: "question-1",
- turnId: nativeId,
- questions: [
- {
- id: "approach",
- header: "Approach",
- question: "Which approach?",
- options: [{ label: "Small" }, { label: "Large" }],
- selection: { mode: "single" },
- },
- ],
- });
- const pending = yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.kind === "user_input",
- );
- const invalid = yield* harness.runtime
- .respondToRuntimeRequest({
- requestId: pending.runtimeRequest.id,
- answers: { approach: ["Small", "Large"] },
- })
- .pipe(Effect.exit);
- assert.strictEqual(invalid._tag, "Failure");
- assert.isFalse(fake.calls.some((call) => call.method === "userInput/answer"));
- yield* harness.runtime.respondToRuntimeRequest({
- requestId: pending.runtimeRequest.id,
- answers: { approach: ["Small", "Keep tests focused"] },
- });
- const response = yield* fake.takeCall("userInput/answer");
- assert.deepStrictEqual(response.params.answers, [
- { questionId: "approach", selectedLabel: "Small", note: "Keep tests focused" },
- ]);
- yield* fake.emit("userInput/settled", {
- userInputId: "question-1",
- turnId: nativeId,
- answers: response.params.answers,
- });
- yield* harness.takeEvent(
- "runtime_request.updated",
- (event) => event.runtimeRequest.status === "resolved",
- );
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("steers only the active native turn and waits for confirmed interruption", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const turn = yield* startConversation(harness, fake);
- const input = yield* turnInput(harness.providerThread);
- const invalid = yield* harness.runtime.steerTurn!({
- threadId: THREAD_ID,
- runId: input.runId,
- providerThread: harness.providerThread,
- providerTurnId: ProviderTurnId.make("unrelated-turn"),
- message: input.message,
- }).pipe(Effect.exit);
- assert.strictEqual(invalid._tag, "Failure");
- assert.isFalse(fake.calls.some((call) => call.method === "turn/steer"));
- yield* harness.runtime.steerTurn!({
- threadId: THREAD_ID,
- runId: input.runId,
- providerThread: harness.providerThread,
- providerTurnId: turn.providerTurn.id,
- message: { ...input.message, text: "Focus on the parser" },
- });
- assert.strictEqual((yield* fake.takeCall("turn/steer")).params.expectedTurnId, turn.nativeId);
- const interrupt = yield* harness.runtime
- .interruptTurn({
- providerThread: harness.providerThread,
- providerTurnId: turn.providerTurn.id,
- })
- .pipe(Effect.forkScoped);
- assert.strictEqual((yield* fake.takeCall("turn/interrupt")).params.turnId, turn.nativeId);
- yield* fake.emit("turn/completed", { turnId: turn.nativeId, terminal: "cancelled" });
- yield* Fiber.join(interrupt);
- assert.strictEqual((yield* harness.takeEvent("turn.terminal")).status, "interrupted");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("resumes the requested native session and reads the same session snapshot", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const nativeItem: MuseItem = {
- itemId: "prior-message",
- turnId: "prior-turn",
- kind: "agentMessage",
- status: "completed",
- revision: 1,
- text: "Previous answer",
- };
- fake.setHistory([nativeItem]);
- const harness = yield* makeHarness(fake, INSTANCE_ID, "saved-session");
- const resumed = yield* fake.takeCall("session/resume");
- assert.strictEqual(resumed.params.sessionId, "saved-session");
- assert.strictEqual(resumed.params.excludeItems, true);
- assert.isFalse(fake.calls.some((call) => call.method === "session/start"));
- assert.strictEqual(harness.providerThread.nativeThreadRef?.nativeId, "saved-session");
- const snapshot = yield* harness.runtime.readThreadSnapshot!({
- providerThread: harness.providerThread,
- });
- assert.strictEqual(snapshot.providerThread.nativeThreadRef?.nativeId, "saved-session");
- assert.deepStrictEqual(snapshot.providerPayload, [nativeItem]);
- assert.strictEqual(snapshot.messages[0]?.text, "Previous answer");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("serializes interruption behind pending turn admission without losing the target", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const admission = pendingPromise>();
- fake.queueResponse("turn/start", admission.promise);
- const starting = yield* harness.runtime
- .startTurn(yield* turnInput(harness.providerThread))
- .pipe(Effect.forkScoped);
- const call = yield* fake.takeCall("turn/start");
- const running = yield* harness.takeEvent(
- "provider_turn.updated",
- (event) => event.providerTurn.status === "running",
- );
- const stopping = yield* harness.runtime
- .interruptTurn({
- providerThread: harness.providerThread,
- providerTurnId: running.providerTurn.id,
- })
- .pipe(Effect.forkScoped);
- admission.resolve({ turnId: call.commandId });
- yield* Fiber.join(starting);
- assert.strictEqual((yield* fake.takeCall("turn/interrupt")).params.turnId, call.commandId);
- yield* fake.emit("turn/completed", { turnId: call.commandId, terminal: "cancelled" });
- yield* Fiber.join(stopping);
- assert.strictEqual((yield* harness.takeEvent("turn.terminal")).status, "interrupted");
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("reports a no-op compaction as a visible failure and permits another turn", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- fake.queueResponse("session/compact", { status: "noop", reason: "no_compactable_history" });
- yield* harness.runtime.compactThread!(yield* turnInput(harness.providerThread));
- const terminal = yield* harness.takeEvent("turn.terminal");
- assert.strictEqual(terminal.status, "failed");
- assert.strictEqual(terminal.threadDisposition, "reusable");
- yield* startConversation(harness, fake, 2);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-
- it.effect("projects native todos with stable identity and removes cancelled steps", () =>
- Effect.gen(function* () {
- const fake = yield* makeFakeMuse();
- const harness = yield* makeHarness(fake);
- const { nativeId } = yield* startConversation(harness, fake);
- // Neither the tool call that wrote the list nor Muse's per-step reminder
- // child may show as a generic tool row.
- yield* fake.emit("item/completed", {
- turnId: nativeId,
- item: {
- itemId: "todo-tool",
- turnId: nativeId,
- kind: "toolCall",
- tool: "write_todos",
- revision: 1,
- status: "completed",
- },
- });
- yield* fake.emit("item/started", {
- turnId: nativeId,
- item: {
- itemId: "reminder",
- turnId: nativeId,
- kind: "reminderChild",
- revision: 1,
- status: "inProgress",
- fallbackText: "Reminder child session",
- },
- });
- yield* fake.emit("session/todoListChanged", {
- items: [
- { text: " Investigate ", status: "completed" },
- { text: "Fix", status: "inProgress" },
- { text: "Discarded", status: "cancelled" },
- { text: " ", status: "pending" },
- ],
- });
- const first = yield* harness.takeEvent(
- "turn_item.updated",
- (event) => event.turnItem.type === "todo_list",
- );
- assert.strictEqual(first.turnItem.type, "todo_list");
- if (first.turnItem.type !== "todo_list") return;
- assert.deepStrictEqual(
- first.turnItem.steps.map(({ text, status }) => ({ text, status })),
- [
- { text: "Investigate", status: "completed" },
- { text: "Fix", status: "running" },
- ],
- );
- const planId = first.turnItem.planId;
- yield* fake.emit("session/todoListChanged", {
- items: [
- { text: "Investigate", status: "completed" },
- { text: "Fix", status: "completed" },
- ],
- });
- const next = yield* harness.takeEvent(
- "turn_item.updated",
- (event) => event.turnItem.type === "todo_list",
- );
- assert.strictEqual(next.turnItem.id, first.turnItem.id);
- assert.strictEqual(next.turnItem.nodeId, first.turnItem.nodeId);
- if (next.turnItem.type !== "todo_list") return;
- assert.strictEqual(next.turnItem.planId, first.turnItem.planId);
- assert.isTrue(
- harness.allEvents.some(
- (event) =>
- event.type === "plan.updated" &&
- event.plan.id === planId &&
- event.plan.status === "completed",
- ),
- );
- assert.isFalse(
- harness.allEvents.some(
- (event) => event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool",
- ),
- );
- assert.isTrue((yield* harness.adapter.getCapabilities()).planning.emitsPlanUpdated);
- assert.isTrue((yield* harness.adapter.getCapabilities()).planning.emitsTodoList);
- }).pipe(Effect.scoped, Effect.provide(testLayer)),
- );
-});
diff --git a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.testkit.ts
index cce14853f6fd026601a719d98d75d8fa58a096c8..f36d2c5959f57804dca3de9f83138e2b45a8f0a6 100644
--- a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.testkit.ts
+++ b/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.testkit.ts
@@ -16,33 +16,31 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { Connection, type DuplexTransport } from "@muse-code/sdk";
import {
- MuseSettings,
ProviderDriverKind,
ProviderInstanceId,
ProviderReplayEntry,
type ProviderReplayEntry as ProviderReplayEntryType,
type ProviderReplayTranscript,
} from "@t3tools/contracts";
+import { MuseSettings } from "@t3tools/provider-muse/settings";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
-import * as ServerConfig from "../../config.ts";
+import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost";
+import { layerTestProviderHost } from "@t3tools/provider-testing/host";
import {
museInitializeParams,
museServeArgs,
type MuseSdkHost,
type MuseSdkHostOptions,
-} from "../../provider/museSdk.ts";
+} from "@t3tools/provider-muse/testing";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";
import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
-import {
- makeReplayServerConfig,
- type OrchestratorV2ProviderReplayHarness,
-} from "../testkit/ProviderReplayHarness.ts";
-import { makeMuseAdapterV2 } from "./MuseAdapterV2.ts";
+import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts";
+import { makeMuseAdapterV2 } from "@t3tools/provider-muse/server";
export const MUSE_PROVIDER_KIND = "muse";
export const MUSE_MSP_REPLAY_PROTOCOL = "muse.msp-jsonl";
@@ -448,10 +446,6 @@ export function layer(input: {
/** Replay needs none; a live recording passes the host environment. */
readonly environment?: NodeJS.ProcessEnv;
}) {
- const layerServerConfig = Layer.effect(
- ServerConfig.ServerConfig,
- makeReplayServerConfig(`muse-${input.scenario}`).pipe(Effect.orDie),
- ).pipe(Layer.provide(NodeServices.layer));
return ProviderAdapterRegistry.layerFromAdaptersEffect(
Effect.gen(function* () {
return [
@@ -460,7 +454,7 @@ export function layer(input: {
settings: DEFAULT_MUSE_SETTINGS,
environment: input.environment ?? {},
idAllocator: yield* IdAllocator.IdAllocatorV2,
- serverConfig: yield* ServerConfig.ServerConfig,
+ host: yield* ProviderHost,
fileSystem: yield* FileSystem.FileSystem,
createHost: input.createHost,
// Same queue the continuation worker drains when the fixture runs it.
@@ -468,7 +462,15 @@ export function layer(input: {
}),
];
}),
- ).pipe(Layer.provide(Layer.mergeAll(layerServerConfig, NodeServices.layer, IdAllocator.layer)));
+ ).pipe(
+ Layer.provide(
+ Layer.mergeAll(
+ layerTestProviderHost().pipe(Layer.provide(NodeServices.layer)),
+ NodeServices.layer,
+ IdAllocator.layer,
+ ),
+ ),
+ );
}
export const MuseOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
diff --git a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.ts
deleted file mode 100644
index 30d7073463a76903bbe73a1953b70b97e6e93386..0000000000000000000000000000000000000000
--- a/apps/server/src/orchestration-v2/Adapters/MuseAdapterV2.ts
+++ /dev/null
@@ -1,2058 +0,0 @@
-import { MspError, type SendUserTurnOptions } from "@muse-code/sdk";
-import {
- MUSE_DEFAULT_MODEL,
- type MuseSettings,
- ProviderDriverKind,
- type ModelSelection,
- type OrchestrationV2ConversationMessage,
- type OrchestrationV2ExecutionNode,
- type OrchestrationV2ProviderCapabilities,
- type OrchestrationV2ProviderSession,
- type OrchestrationV2ProviderThread,
- type OrchestrationV2ProviderTurn,
- type OrchestrationV2RuntimeRequest,
- type OrchestrationV2Subagent,
- type OrchestrationV2TurnItemStatus,
- type NodeId,
- type OrchestrationV2TurnItem,
- type ProviderInstanceId,
- type PlanId,
- type OrchestrationV2PlanStep,
- type ProviderApprovalDecision,
- type RuntimeRequestId,
- type ServerProviderModel,
-} from "@t3tools/contracts";
-import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
-import * as Cause from "effect/Cause";
-import * as DateTime from "effect/DateTime";
-import * as Deferred from "effect/Deferred";
-import * as Effect from "effect/Effect";
-import * as Option from "effect/Option";
-import * as FileSystem from "effect/FileSystem";
-import * as Queue from "effect/Queue";
-import * as Schema from "effect/Schema";
-import * as Semaphore from "effect/Semaphore";
-import * as Scope from "effect/Scope";
-import * as Stream from "effect/Stream";
-
-import { resolveAttachmentPath } from "../../attachmentStore.ts";
-import * as ServerConfig from "../../config.ts";
-import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
-import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
-import {
- museModelCapabilities,
- resolveMuseReasoningEffort,
-} from "../../provider/museModelCatalog.ts";
-import {
- MuseApproval,
- MuseCompactResult,
- MuseContextUsage,
- MuseDelta,
- MuseItemEvent,
- MuseSessionResult,
- MuseTokenUsageEvent,
- MuseTodoList,
- MuseTurnRetryScheduled,
- MuseTurnCompleted,
- MuseTurnStartResult,
- MuseUserInput,
- museApprovalChoices,
- museApprovalDecision,
- museApprovalOptions,
- type MuseItem,
-} from "../../provider/museProtocol.ts";
-import {
- createMuseSdkHostEffect,
- museApprovalMode,
- type createMuseSdkHost,
- type MuseSdkHost,
-} from "../../provider/museSdk.ts";
-import type { EventNdjsonLogger } from "../../provider/EventNdjsonLogger.ts";
-import {
- providerMessageTextWithAttachmentPaths,
- isProviderNativeImageAttachment,
-} from "@t3tools/provider-core/server/attachmentPrompt";
-import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import {
- ProviderAdapterEnsureThreadError,
- ProviderAdapterForkThreadError,
- ProviderAdapterInterruptError,
- ProviderAdapterOpenSessionError,
- ProviderAdapterProtocolError,
- ProviderAdapterReadThreadSnapshotError,
- ProviderAdapterResumeThreadError,
- ProviderAdapterRollbackThreadError,
- ProviderAdapterRuntimeRequestResponseError,
- ProviderAdapterSteerRunError,
- ProviderAdapterTurnStartError,
- ProviderAdapterV2,
- type ProviderAdapterV2Error,
- type ProviderAdapterV2Event,
- type ProviderAdapterV2EnsureThreadInput,
- type ProviderAdapterV2OpenSessionInput,
- type ProviderAdapterV2SessionRuntime,
- type ProviderAdapterV2Shape,
- type ProviderAdapterV2ThreadSnapshot,
- type ProviderAdapterV2TurnInput,
- type ProviderAdapterV2TurnMessage,
-} from "@t3tools/provider-core/server/ProviderAdapter";
-import {
- backgroundWorkNotification,
- type BackgroundWorkReport,
-} from "@t3tools/provider-core/server/notification";
-import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
-import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
-import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
-import { museItemStatus, museToolPresentation } from "./MuseItemPresentation.ts";
-
-const MUSE_PROVIDER = ProviderDriverKind.make("muse");
-const isOpenSessionError = Schema.is(ProviderAdapterOpenSessionError);
-const isProtocolError = Schema.is(ProviderAdapterProtocolError);
-
-const MuseProviderCapabilitiesV2 = {
- runtimePolicy: { enforcement: "native" },
- sessions: {
- supportsMultipleProviderThreadsPerSession: false,
- supportsModelSwitchInSession: true,
- supportsProviderSwitchingViaHandoff: true,
- supportsRuntimeModeSwitchInSession: false,
- pendingRequestsSurviveRestart: false,
- },
- threads: {
- canCreateEmptyThread: true,
- canReadThreadSnapshot: true,
- // Muse 1.2.1/1.3.0 reject tested completed-turn fork boundaries with InvalidCut.
- // Public forks use portable context handoff until native boundaries are validated.
- canRollbackThread: false,
- canForkThread: false,
- canForkFromTurn: false,
- canForkFromSubagentThread: false,
- exposesNativeThreadId: true,
- },
- turns: {
- exposesNativeTurnId: true,
- emitsTurnStarted: true,
- emitsTurnCompleted: true,
- supportsInterrupt: true,
- supportsActiveSteering: true,
- supportsSteeringByInterruptRestart: false,
- supportsQueuedMessages: true,
- terminalStatusQuality: "strong",
- },
- streaming: {
- streamsAssistantText: true,
- streamsReasoning: true,
- streamsToolOutput: true,
- streamsPlanText: false,
- emitsMessageCompleted: true,
- },
- tools: {
- exposesToolItemIds: true,
- emitsToolStarted: true,
- emitsToolCompleted: true,
- emitsToolOutput: true,
- supportsMcpTools: true,
- supportsDynamicToolCallbacks: false,
- },
- approvals: {
- supportsCommandApproval: true,
- supportsFileReadApproval: true,
- supportsFileChangeApproval: true,
- supportsApplyPatchApproval: false,
- approvalsHaveNativeRequestIds: true,
- approvalCallbacksAreLiveOnly: true,
- approvalsCanOriginateFromSubagents: true,
- },
- planning: {
- emitsPlanUpdated: true,
- emitsTodoList: true,
- emitsProposedPlan: false,
- supportsStructuredQuestions: true,
- planDeltasHaveItemIds: false,
- },
- subagents: {
- supportsSubagents: false,
- exposesSubagentThreadIds: false,
- emitsSubagentLifecycle: false,
- canWaitForSubagents: false,
- canCloseSubagents: false,
- canForkSubagentThread: false,
- },
- context: {
- acceptsSystemContext: false,
- acceptsDeveloperContext: false,
- acceptsSyntheticUserContext: true,
- canGenerateSummaries: false,
- canConsumeHandoffSummaries: true,
- supportsDeltaHandoff: true,
- supportsFullThreadHandoff: true,
- maxRecommendedHandoffChars: null,
- },
- checkpointing: {
- appCanCheckpointFilesystem: true,
- supportsNestedCheckpointScopes: false,
- providerCanRollbackConversation: false,
- providerRollbackReturnsSnapshot: false,
- providerCanReadConversationSnapshot: true,
- },
- identity: {
- nativeThreadIds: "strong",
- nativeTurnIds: "strong",
- nativeItemIds: "strong",
- nativeRequestIds: "strong",
- },
-} satisfies OrchestrationV2ProviderCapabilities;
-
-export interface MuseAdapterV2Options {
- readonly instanceId: ProviderInstanceId;
- readonly settings: MuseSettings;
- readonly environment: NodeJS.ProcessEnv;
- readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
- readonly serverConfig: ServerConfig.ServerConfig["Service"];
- readonly fileSystem: FileSystem.FileSystem;
- readonly modelCatalog?: Effect.Effect>;
- readonly createHost?: typeof createMuseSdkHost;
- readonly requestTimeoutMs?: number;
- readonly nativeEventLogger?: EventNdjsonLogger;
- /** Where a turn Muse starts on its own (a finished workflow's report) asks for its run. */
- readonly continuationRequests?: {
- readonly offer: (
- request: ProviderContinuationRequests.ProviderContinuationRequest,
- ) => Effect.Effect;
- };
-}
-
-interface ActiveTurn {
- readonly input: ProviderAdapterV2TurnInput;
- providerTurn: OrchestrationV2ProviderTurn;
- readonly nativeId: string;
- /** Turns Muse started on its own while this run was active, which this run shows. */
- readonly joined: Set;
- readonly items: Map;
- readonly ordinals: Map;
- readonly started: Map;
- readonly dirty: Set;
- readonly settledRequests: Set;
- readonly done: Deferred.Deferred;
- todoPlanId?: PlanId;
- nextOrdinal: number;
- flushScheduled: boolean;
- interruptRequested: boolean;
- compact: boolean;
-}
-
-interface PendingRequest {
- readonly native:
- | { type: "approval"; value: MuseApproval }
- | { type: "question"; value: MuseUserInput };
- request: OrchestrationV2RuntimeRequest;
- node: OrchestrationV2ExecutionNode;
- item: OrchestrationV2TurnItem;
- /** What T3 answered, recorded on the request once Muse confirms it. */
- response?: Pick;
-}
-
-const nativeRef = (nativeId: string) => ({
- driver: MUSE_PROVIDER,
- nativeId,
- strength: "strong" as const,
-});
-const recordSchema = Schema.Record(Schema.String, Schema.Unknown);
-// Notifications that only add detail. A malformed one is skipped instead of ending the session.
-const INFORMATIONAL_NOTIFICATIONS = new Set([
- "session/contextUsage",
- "session/tokenUsage",
- "session/todoListChanged",
- "turn/retryScheduled",
-]);
-// Tools whose results already show as their own rows: T3's todo list and question
-// rows, and the workflow item a `workflow` call launches.
-const TOOLS_WITH_NATIVE_ROWS = new Set(["write_todos", "request_user_input", "workflow"]);
-const responseAnswerSchema = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
-
-/** One scoped Muse host owns one native session; the orchestrator owns app runs and queuing. */
-export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapterV2Shape {
- const { idAllocator } = options;
- const protocolError = (detail: string, payload?: unknown) =>
- new ProviderAdapterProtocolError({
- driver: MUSE_PROVIDER,
- detail,
- ...(payload === undefined ? {} : { payload }),
- });
- return ProviderAdapterV2.of({
- instanceId: options.instanceId,
- driver: MUSE_PROVIDER,
- getCapabilities: () => Effect.succeed(MuseProviderCapabilitiesV2),
- planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
- openSession: Effect.fn("MuseAdapterV2.openSession")(function* (
- input: ProviderAdapterV2OpenSessionInput,
- ) {
- if (!options.settings.enabled) return yield* protocolError("Muse Code is disabled");
- if (input.runtimePolicy.interactionMode === "plan")
- return yield* protocolError("Muse Code does not support dedicated Plan mode");
- const scope = yield* Effect.scope;
- // Muse rejects non-canonical workspace roots (for example macOS /tmp) and
- // reports canonical paths in approvals, so resolve symlinks once here.
- const requestedCwd = input.runtimePolicy.cwd ?? options.serverConfig.cwd;
- const cwd = yield* options.fileSystem
- .realPath(requestedCwd)
- .pipe(Effect.orElseSucceed(() => requestedCwd));
- const now = yield* DateTime.now;
- let session: OrchestrationV2ProviderSession = {
- id: input.providerSessionId,
- driver: MUSE_PROVIDER,
- providerInstanceId: options.instanceId,
- status: "ready",
- cwd,
- model: input.modelSelection.model,
- capabilities: MuseProviderCapabilitiesV2,
- createdAt: now,
- updatedAt: now,
- lastError: null,
- };
- const events = yield* Queue.unbounded<
- ProviderAdapterV2Event,
- ProviderAdapterV2Error | Cause.Done
- >();
- type Inbox =
- | { type: "notification"; method: string; params: unknown; epoch: number }
- | { type: "failure"; cause: unknown; epoch: number }
- | { type: "flush"; turn: ActiveTurn };
- const inbox = yield* Queue.unbounded();
- const commands = yield* Semaphore.make(1);
- const eventPermit = yield* Semaphore.make(1);
- let host: MuseSdkHost;
- let hostEpoch = 0;
- let closed = false;
- let broken = false;
- let nativeSessionId: string | undefined;
- let thread: OrchestrationV2ProviderThread | undefined;
- let active: ActiveTurn | undefined;
- // The last finished turn, so context usage reported after it still lands on it.
- let lastProviderTurn: OrchestrationV2ProviderTurn | undefined;
- const agentEndedAt = new Map();
- // "default" is the catalog's default model, from the provider snapshot. Without
- // one, Muse keeps whatever model the session already runs.
- const resolveModel = Effect.fnUntraced(function* (selection: ModelSelection) {
- if (selection.model !== MUSE_DEFAULT_MODEL) return selection.model;
- const catalog = options.modelCatalog ? yield* options.modelCatalog : [];
- return catalog.find((model) => model.isDefault && !model.isCustom)?.slug;
- });
- const pending = new Map();
- const seenTerminals = new Set();
- // Workflows and subagents that outlive their turn, by item id, with the turn that owns them.
- const observedChildren = new Map();
- // Background work that finished since the last turn, reported to the turn Muse starts for it.
- const finishedBackground: Array = [];
- // A turn Muse started on its own, held until the continuation run T3 opens for it takes it.
- let wake:
- | { readonly nativeId: string; readonly events: Array<[string, unknown]> }
- | undefined;
- const emit = (event: ProviderAdapterV2Event) =>
- Queue.offer(events, event).pipe(Effect.asVoid);
- const decode = (schema: Schema.Codec, data: unknown) =>
- Schema.decodeUnknownEffect(schema)(data).pipe(
- Effect.mapError((cause) => protocolError("Invalid Muse protocol response", cause)),
- );
- const request = (
- method: string,
- params: Record,
- command = true,
- commandId?: string,
- ) =>
- Effect.tryPromise({
- try: () =>
- command
- ? host.connection.command(
- method,
- { ...(nativeSessionId ? { sessionId: nativeSessionId } : {}), ...params },
- {
- ...(method === "turn/start" ? {} : { maxAttempts: 1 }),
- ...(commandId ? { commandId } : {}),
- },
- )
- : host.connection.request(method, {
- ...(nativeSessionId ? { sessionId: nativeSessionId } : {}),
- ...params,
- }),
- catch: (cause) =>
- protocolError(
- cause instanceof Error && cause.message
- ? `Muse ${method} failed: ${cause.message}`
- : `Muse ${method} failed`,
- cause,
- ),
- }).pipe(
- Effect.timeout(options.requestTimeoutMs ?? 30_000),
- Effect.catchTags({
- TimeoutError: (cause) =>
- Effect.gen(function* () {
- yield* eventPermit.withPermits(1)(failHost(cause));
- return yield* protocolError(`Muse ${method} timed out; its host was closed`, cause);
- }),
- }),
- );
- const updateSession = Effect.fnUntraced(function* (
- status: OrchestrationV2ProviderSession["status"],
- lastError: string | null = null,
- ) {
- session = { ...session, status, lastError, updatedAt: yield* DateTime.now };
- yield* emit({
- type: "provider_session.updated",
- driver: MUSE_PROVIDER,
- providerSession: session,
- });
- });
- const updateThread = Effect.fnUntraced(function* (
- patch: Partial,
- ) {
- if (!thread) return;
- thread = { ...thread, ...patch, updatedAt: yield* DateTime.now };
- yield* emit({
- type: "provider_thread.updated",
- driver: MUSE_PROVIDER,
- providerThread: thread,
- });
- });
- const itemIdentity = (turn: ActiveTurn, nativeId: string) =>
- `${options.instanceId}:${turn.input.providerThread.id}:${turn.nativeId}:${nativeId}`;
- const baseItem = (turn: ActiveTurn, nativeId: string, time: DateTime.Utc) => {
- if (!turn.ordinals.has(nativeId)) turn.ordinals.set(nativeId, turn.nextOrdinal++);
- if (!turn.started.has(nativeId)) turn.started.set(nativeId, time);
- const identity = itemIdentity(turn, nativeId);
- return {
- id: idAllocator.derive.turnItemFromProviderItem({
- driver: MUSE_PROVIDER,
- nativeItemId: identity,
- }),
- nodeId: idAllocator.derive.nodeFromProviderItem({
- driver: MUSE_PROVIDER,
- nativeItemId: identity,
- }),
- threadId: turn.input.threadId,
- runId: turn.input.runId,
- providerThreadId: turn.input.providerThread.id,
- providerTurnId: turn.providerTurn.id,
- nativeItemRef: nativeRef(nativeId),
- parentItemId: null,
- ordinal: turn.ordinals.get(nativeId)!,
- startedAt: turn.started.get(nativeId)!,
- updatedAt: time,
- };
- };
- /** Lists running workflows and subagents on the thread, so it shows it still waits on them. */
- const syncBackground = Effect.fnUntraced(function* () {
- yield* updateThread({
- pendingBackgroundTasks: [...observedChildren].flatMap(([itemId, owner]) => {
- const item = owner.items.get(itemId);
- const description = item?.objective ?? item?.entryId ?? item?.fallbackText;
- return [
- {
- taskId: itemId,
- kind:
- item?.kind === "subagent" ? ("subagent" as const) : ("background_task" as const),
- ...(description ? { description } : {}),
- },
- ];
- }),
- });
- });
- const publishItem = Effect.fnUntraced(function* (
- turn: ActiveTurn,
- item: MuseItem,
- terminal?: OrchestrationV2TurnItem["status"],
- ) {
- // Muse's skill-reminder child runs on every model step; it is housekeeping, not work.
- if (item.kind === "userMessage" || item.kind === "reminderChild") return;
- // A failed call keeps its row: it may have no native row to show its error.
- if (
- item.kind === "toolCall" &&
- item.tool &&
- TOOLS_WITH_NATIVE_ROWS.has(item.tool) &&
- item.status !== "failed"
- )
- return;
- const time = yield* DateTime.now;
- const status = terminal ?? museItemStatus(item);
- if (item.kind === "subagent" || item.kind === "workflow") {
- const wasRunning = observedChildren.has(item.itemId);
- if (status === "running") observedChildren.set(item.itemId, turn);
- else if (observedChildren.delete(item.itemId) && active !== turn)
- finishedBackground.push({
- kind: item.kind === "subagent" ? "subagent" : "background_task",
- label: item.objective ?? item.entryId ?? item.fallbackText,
- outcome:
- status === "completed"
- ? "completed"
- : status === "cancelled"
- ? "cancelled"
- : "failed",
- });
- if (wasRunning !== observedChildren.has(item.itemId)) yield* syncBackground();
- }
- const streaming = status === "running";
- const base = {
- ...baseItem(turn, item.itemId, time),
- status,
- completedAt: streaming ? null : time,
- title: item.tool ?? null,
- };
- const text = item.text ?? item.summary?.join("\n") ?? item.fallbackText ?? "";
- let turnItem: OrchestrationV2TurnItem;
- if (item.kind === "agentMessage") {
- const messageId = idAllocator.derive.messageFromProviderItem({
- driver: MUSE_PROVIDER,
- nativeItemId: itemIdentity(turn, item.itemId),
- });
- const message: OrchestrationV2ConversationMessage = {
- id: messageId,
- threadId: turn.input.threadId,
- runId: turn.input.runId,
- nodeId: base.nodeId,
- role: "assistant",
- text,
- attachments: [],
- streaming,
- createdBy: "agent",
- creationSource: "provider",
- createdAt: base.startedAt,
- updatedAt: time,
- };
- yield* emit({ type: "message.updated", driver: MUSE_PROVIDER, message });
- turnItem = { ...base, type: "assistant_message", messageId, text, streaming };
- } else if (item.kind === "reasoning") {
- turnItem = { ...base, type: "reasoning", text, streaming };
- } else if (item.kind === "compaction") {
- turnItem = {
- ...base,
- type: "compaction",
- driver: MUSE_PROVIDER,
- summary: text || item.reason,
- };
- } else {
- turnItem = { ...base, ...museToolPresentation(item, status) };
- }
- yield* emit({
- type: "node.updated",
- driver: MUSE_PROVIDER,
- node: {
- id: base.nodeId,
- threadId: base.threadId,
- runId: base.runId,
- parentNodeId: turn.input.rootNodeId,
- rootNodeId: turn.input.rootNodeId,
- kind:
- item.kind === "agentMessage"
- ? "assistant_message"
- : item.kind === "reasoning"
- ? "reasoning"
- : "tool_call",
- status,
- countsForRun: false,
- providerThreadId: base.providerThreadId,
- providerTurnId: base.providerTurnId,
- nativeItemRef: base.nativeItemRef,
- runtimeRequestId: null,
- checkpointScopeId: null,
- startedAt: base.startedAt,
- completedAt: base.completedAt,
- },
- });
- yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem });
- if (item.kind === "workflow") yield* publishWorkflowAgents(turn, item, base, status);
- });
- /**
- * Shows each workflow child as a native subagent under the workflow row. Muse
- * reports only their status, label and outcome, not their conversations, so
- * they get no child thread.
- */
- const publishWorkflowAgents = Effect.fnUntraced(function* (
- turn: ActiveTurn,
- item: MuseItem,
- workflow: { readonly id: OrchestrationV2TurnItem["id"]; readonly nodeId: NodeId },
- workflowStatus: OrchestrationV2TurnItemStatus,
- ) {
- const latest = new Map((item.children ?? []).map((child) => [child.childId, child]));
- for (const [index, child] of [...latest.values()].entries()) {
- const time = yield* DateTime.now;
- const base = baseItem(turn, `${item.itemId}:agent:${child.childId}`, time);
- const status: OrchestrationV2TurnItemStatus =
- child.terminal === "completed"
- ? "completed"
- : child.terminal === "cancelled"
- ? "cancelled"
- : child.terminal !== undefined
- ? "failed"
- : workflowStatus !== "running"
- ? workflowStatus
- : child.status === "scheduled"
- ? "pending"
- : "running";
- const settled = status !== "running" && status !== "pending";
- // Children are re-sent on every workflow change; keep each one's first end time.
- const endKey = `${item.itemId}:${child.childId}:${child.attempt}`;
- if (settled && !agentEndedAt.has(endKey)) agentEndedAt.set(endKey, time);
- const title = child.label?.trim() || `Agent ${index + 1}`;
- const result = child.failureReason ?? null;
- const subagent: OrchestrationV2Subagent = {
- id: base.nodeId,
- threadId: base.threadId,
- runId: base.runId,
- parentNodeId: workflow.nodeId,
- origin: "provider_native",
- createdBy: "agent",
- driver: MUSE_PROVIDER,
- providerInstanceId: options.instanceId,
- providerThreadId: base.providerThreadId,
- childThreadId: null,
- nativeTaskRef: nativeRef(child.childId),
- prompt: title,
- title,
- model: null,
- status,
- result,
- startedAt: base.startedAt,
- completedAt: settled ? (agentEndedAt.get(endKey) ?? time) : null,
- updatedAt: time,
- };
- yield* emit({ type: "subagent.updated", driver: MUSE_PROVIDER, subagent });
- yield* emit({
- type: "node.updated",
- driver: MUSE_PROVIDER,
- node: {
- id: base.nodeId,
- threadId: base.threadId,
- runId: base.runId,
- parentNodeId: workflow.nodeId,
- rootNodeId: turn.input.rootNodeId,
- kind: "subagent",
- status,
- countsForRun: false,
- providerThreadId: base.providerThreadId,
- providerTurnId: base.providerTurnId,
- nativeItemRef: subagent.nativeTaskRef,
- runtimeRequestId: null,
- checkpointScopeId: null,
- startedAt: base.startedAt,
- completedAt: subagent.completedAt,
- },
- });
- yield* emit({
- type: "turn_item.updated",
- driver: MUSE_PROVIDER,
- turnItem: {
- ...base,
- nativeItemRef: subagent.nativeTaskRef,
- parentItemId: workflow.id,
- type: "subagent",
- status,
- title,
- completedAt: subagent.completedAt,
- subagentId: subagent.id,
- origin: "provider_native",
- driver: MUSE_PROVIDER,
- providerInstanceId: options.instanceId,
- childThreadId: null,
- prompt: title,
- result,
- },
- });
- }
- });
- const settleObservedChildren = Effect.fnUntraced(function* (
- status: "failed" | "cancelled" | "interrupted",
- ) {
- for (const [itemId, owner] of observedChildren) {
- const item = owner.items.get(itemId);
- if (item) yield* publishItem(owner, item, status);
- }
- observedChildren.clear();
- if (thread?.pendingBackgroundTasks?.length) yield* syncBackground();
- // Only workflow children's requests outlive a turn; they end with the workflow.
- for (const entry of pending.values()) yield* resolvePending(entry, "cancelled");
- });
- const resolvePending = Effect.fnUntraced(function* (
- entry: PendingRequest,
- status: "resolved" | "cancelled",
- ) {
- const time = yield* DateTime.now;
- entry.request = {
- ...entry.request,
- ...(status === "resolved" ? entry.response : undefined),
- status,
- resolvedAt: time,
- responseCapability: { type: "not_resumable", reason: "This Muse request has ended." },
- };
- entry.node = {
- ...entry.node,
- status: status === "resolved" ? "completed" : "cancelled",
- completedAt: time,
- };
- entry.item = {
- ...entry.item,
- status: status === "resolved" ? "completed" : "cancelled",
- completedAt: time,
- updatedAt: time,
- };
- pending.delete(entry.request.id);
- yield* emit({
- type: "runtime_request.updated",
- driver: MUSE_PROVIDER,
- threadId: input.threadId,
- runtimeRequest: entry.request,
- });
- yield* emit({ type: "node.updated", driver: MUSE_PROVIDER, node: entry.node });
- yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem: entry.item });
- });
- const finish = Effect.fnUntraced(function* (
- turn: ActiveTurn,
- status: "completed" | "cancelled" | "interrupted" | "failed",
- detail?: string,
- disposition: "reusable" | "broken" = "reusable",
- ) {
- if (active !== turn || seenTerminals.has(turn.nativeId)) return;
- seenTerminals.add(turn.nativeId);
- const completedAt = yield* DateTime.now;
- for (const item of turn.items.values()) {
- if (
- (item.kind === "subagent" || item.kind === "workflow") &&
- item.status === "inProgress" &&
- disposition === "reusable"
- )
- continue;
- if (turn.dirty.has(item.itemId) || item.status === "inProgress")
- yield* publishItem(turn, item, item.status === "inProgress" ? status : undefined);
- }
- turn.dirty.clear();
- // A workflow child's request is runless and keeps waiting after the turn ends.
- for (const entry of pending.values())
- if (entry.request.providerTurnId !== null) yield* resolvePending(entry, "cancelled");
- turn.providerTurn = { ...turn.providerTurn, status, completedAt };
- lastProviderTurn = turn.providerTurn;
- yield* emit({
- type: "provider_turn.updated",
- driver: MUSE_PROVIDER,
- threadId: turn.input.threadId,
- providerTurn: turn.providerTurn,
- });
- yield* updateThread({
- status: disposition === "broken" ? "error" : "idle",
- ...(!turn.compact && status === "completed"
- ? { nativeConversationHeadRef: nativeRef(turn.nativeId) }
- : {}),
- });
- yield* updateSession(disposition === "broken" ? "error" : "ready", detail ?? null);
- active = undefined;
- if (status === "failed") {
- const failure = makeProviderFailure({
- class: disposition === "broken" ? "transport_error" : "provider_error",
- message: detail ?? "Muse turn failed.",
- });
- const base = baseItem(turn, `failure:${turn.nativeId}`, completedAt);
- yield* emit({
- type: "turn_item.updated",
- driver: MUSE_PROVIDER,
- turnItem: {
- ...base,
- type: "error",
- status: "failed",
- title: null,
- completedAt,
- failure,
- },
- });
- yield* emit({
- type: "turn.terminal",
- driver: MUSE_PROVIDER,
- providerThreadId: turn.providerTurn.providerThreadId,
- providerTurnId: turn.providerTurn.id,
- runOrdinal: turn.input.runOrdinal,
- status,
- failure,
- failureItemOrdinal: base.ordinal,
- threadDisposition: disposition,
- });
- } else {
- yield* emit({
- type: "turn.terminal",
- driver: MUSE_PROVIDER,
- providerThreadId: turn.providerTurn.providerThreadId,
- providerTurnId: turn.providerTurn.id,
- runOrdinal: turn.input.runOrdinal,
- status,
- failure: null,
- threadDisposition: disposition,
- });
- }
- yield* Deferred.succeed(turn.done, undefined);
- });
- const failHost = Effect.fnUntraced(function* (cause: unknown) {
- if (closed || broken) return;
- broken = true;
- const rootCause = Cause.isCause(cause) ? Cause.squash(cause) : cause;
- const failure = isOpenSessionError(rootCause) ? rootCause.cause : rootCause;
- const detail = isProtocolError(failure)
- ? failure.detail
- : failure instanceof Error
- ? failure.message
- : "Muse transport failed.";
- if (active) yield* finish(active, "failed", detail, "broken");
- else {
- yield* updateThread({ status: "error" });
- yield* updateSession("error", detail);
- }
- yield* settleObservedChildren("failed");
- yield* Queue.fail(events, protocolError(detail, cause));
- // Report the failure first; a hung host can take a while to close.
- const failed = host;
- yield* Effect.tryPromise(() => failed.close()).pipe(Effect.ignore, Effect.forkIn(scope));
- });
- const owns = (turn: ActiveTurn, turnId: string) =>
- turnId === turn.nativeId || turn.joined.has(turnId);
- /** Sends T3's decision for a pending approval to Muse. */
- const decideApproval = Effect.fnUntraced(function* (
- entry: PendingRequest,
- decision: ProviderApprovalDecision | undefined,
- ) {
- if (entry.native.type !== "approval") return;
- const approval = entry.native.value;
- const choice = decision && museApprovalChoices(approval).get(decision);
- if (!choice) return yield* protocolError("Muse did not offer this approval decision");
- // Recorded first: Muse can settle the approval before it acknowledges the command.
- entry.response = { decision };
- yield* request("approval/decide", {
- approvalId: approval.approvalId,
- requirementId: approval.currentRequirementId,
- choiceId: choice.choiceId,
- }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response)));
- });
- /**
- * Shows a Muse approval or question as a T3 request. A workflow child's approval
- * is runless: the turn that started the workflow may end long before the child
- * asks, and T3 keeps a runless request on the thread until Muse settles it.
- */
- const publishRequest = Effect.fnUntraced(function* (native: PendingRequest["native"]) {
- const child = native.type === "approval" && native.value.subagentOrigin !== undefined;
- const turn = child ? undefined : active;
- if (
- !thread ||
- (!child && !turn) ||
- native.value.sessionId !== nativeSessionId ||
- (turn && native.value.turnId && !owns(turn, native.value.turnId))
- )
- return;
- const nativeId =
- native.type === "approval" ? native.value.approvalId : native.value.userInputId;
- if (turn?.settledRequests.has(`${native.type}:${nativeId}`)) return;
- const previous = [...pending.values()].find(
- (entry) =>
- entry.native.type === native.type &&
- entry.request.nativeRequestRef?.nativeId === nativeId,
- );
- const providerTurnId = turn?.providerTurn.id ?? null;
- const threadId = turn?.input.threadId ?? thread.appThreadId ?? input.threadId;
- const requestId =
- previous?.request.id ??
- (yield* idAllocator.allocate
- .runtimeRequest({
- driver: MUSE_PROVIDER,
- ...(providerTurnId ? { providerTurnId } : {}),
- nativeRequestId: nativeId,
- })
- .pipe(
- Effect.mapError((cause) => protocolError("Cannot allocate Muse request", cause)),
- ));
- const time = yield* DateTime.now;
- const nodeId = idAllocator.derive.approvalNode({ requestId });
- const subject = native.type === "approval" ? native.value.subject : undefined;
- const kind = !subject
- ? "user_input"
- : subject.kind === "shell"
- ? "command"
- : subject.kind !== "fileAccess"
- ? "permission"
- : subject.access === "read"
- ? "file-read"
- : "file-change";
- const runtimeRequest: OrchestrationV2RuntimeRequest = {
- id: requestId,
- nodeId,
- providerTurnId,
- nativeRequestRef: nativeRef(nativeId),
- kind,
- status: "pending",
- responseCapability: { type: "live", providerSessionId: input.providerSessionId },
- createdAt: previous?.request.createdAt ?? time,
- resolvedAt: null,
- };
- const node: OrchestrationV2ExecutionNode = {
- id: nodeId,
- threadId,
- runId: turn?.input.runId ?? null,
- parentNodeId: turn?.input.rootNodeId ?? null,
- rootNodeId: turn?.input.rootNodeId ?? nodeId,
- kind: native.type === "approval" ? "approval_request" : "user_input_request",
- status: "waiting",
- countsForRun: false,
- providerThreadId: thread.id,
- providerTurnId,
- nativeItemRef: nativeRef(nativeId),
- runtimeRequestId: requestId,
- checkpointScopeId: null,
- startedAt: runtimeRequest.createdAt,
- completedAt: null,
- };
- const latestTurn = active?.providerTurn ?? lastProviderTurn;
- const base = {
- ...(turn
- ? baseItem(turn, `request:${nativeId}`, time)
- : {
- threadId,
- runId: null,
- providerThreadId: thread.id,
- providerTurnId: null,
- nativeItemRef: nativeRef(nativeId),
- parentItemId: null,
- // Sorts after the latest turn's items.
- ordinal: previous?.item.ordinal ?? ((latestTurn?.ordinal ?? 0) + 1) * 100,
- startedAt: runtimeRequest.createdAt,
- updatedAt: time,
- }),
- id: idAllocator.derive.approvalTurnItem({ requestId }),
- nodeId,
- status: "waiting" as const,
- title: null,
- completedAt: null,
- };
- const item: OrchestrationV2TurnItem =
- native.type === "approval"
- ? {
- ...base,
- type: "approval_request",
- requestId,
- requestKind: kind === "user_input" ? "command" : kind,
- prompt:
- native.value.subject.command ??
- native.value.subject.path ??
- native.value.subject.host ??
- native.value.subject.target ??
- native.value.toolName ??
- "Muse requests permission",
- options: museApprovalOptions(native.value),
- }
- : {
- ...base,
- type: "user_input_request",
- requestId,
- questions: native.value.questions.map((question) => ({
- id: question.id,
- header: question.header || "Question",
- question: question.question,
- options: question.options.map((option) => ({
- label: option.label,
- description: option.description || option.label,
- })),
- multiSelect: question.selection.mode === "multiple",
- allowCustomAnswer: true,
- required: true,
- })),
- };
- const entry: PendingRequest = { native, request: runtimeRequest, node, item };
- pending.set(requestId, entry);
- yield* emit({
- type: "runtime_request.updated",
- driver: MUSE_PROVIDER,
- threadId,
- runtimeRequest,
- });
- yield* emit({ type: "node.updated", driver: MUSE_PROVIDER, node });
- yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem: item });
- yield* updateSession("waiting");
- // Muse does not give workflow children the session's allowAll mode, so in full
- // access T3 approves for them. If Muse rejects that, the request waits for the user.
- if (child && input.runtimePolicy.runtimeMode === "full-access")
- yield* decideApproval(entry, "accept").pipe(Effect.ignore, Effect.forkIn(scope));
- });
- /** Approval and question events; `turn` is undefined for a workflow child's approval. */
- const handleRequestEvent = Effect.fnUntraced(function* (
- method: string,
- params: Record,
- turn: ActiveTurn | undefined,
- ) {
- switch (method) {
- case "approval/requested":
- case "approval/updated":
- if (
- method === "approval/updated" &&
- ![...pending.values()].some(
- (entry) =>
- entry.native.type === "approval" &&
- entry.native.value.approvalId === params.approvalId,
- )
- )
- return;
- return yield* publishRequest({
- type: "approval",
- value: yield* decode(MuseApproval, params),
- });
- case "userInput/requested":
- return yield* publishRequest({
- type: "question",
- value: yield* decode(MuseUserInput, params),
- });
- case "approval/resolved":
- case "userInput/settled": {
- const id = method === "approval/resolved" ? params.approvalId : params.userInputId;
- if (typeof id === "string")
- turn?.settledRequests.add(
- `${method === "approval/resolved" ? "approval" : "question"}:${id}`,
- );
- const entry = [...pending.values()].find(
- (candidate) => candidate.request.nativeRequestRef?.nativeId === id,
- );
- if (entry) {
- // An approval settled outside T3 still shows Muse's own decision.
- const native =
- method === "approval/resolved" && typeof params.decision === "string"
- ? museApprovalDecision({ decision: params.decision, scope: "once" })
- : undefined;
- if (!entry.response && native) entry.response = { decision: native };
- yield* resolvePending(entry, "resolved");
- }
- if (!pending.size) yield* updateSession(active ? "running" : "ready");
- }
- }
- });
- const handleNotification = Effect.fnUntraced(function* (method: string, data: unknown) {
- const params = yield* decode(recordSchema, data);
- if (params.sessionId !== nativeSessionId) return;
- if (method === "view/gap")
- return yield* protocolError("Muse delivery gap requires session recovery");
- if (method === "session/contextUsage") {
- const usage = yield* decode(MuseContextUsage, params);
- const currentTurn = active?.providerTurn ?? lastProviderTurn;
- const updatedAt = DateTime.formatIso(yield* DateTime.now);
- const tokenUsage = {
- ...currentTurn?.tokenUsage,
- usedTokens: usage.usedTokens,
- maxTokens: usage.windowTokens ?? currentTurn?.tokenUsage?.maxTokens ?? undefined,
- updatedAt,
- };
- yield* updateThread({ contextUsage: tokenUsage });
- if (currentTurn) {
- const updated = { ...currentTurn, tokenUsage };
- if (active) active.providerTurn = updated;
- else lastProviderTurn = updated;
- yield* emit({
- type: "provider_turn.updated",
- driver: MUSE_PROVIDER,
- providerTurn: updated,
- });
- }
- return;
- }
- const itemEvent = ["item/started", "item/updated", "item/completed"].includes(method)
- ? (yield* decode(MuseItemEvent, params)).item
- : undefined;
- if (itemEvent) {
- const owner = observedChildren.get(itemEvent.itemId);
- if (owner && owner !== active && itemEvent.turnId === owner.nativeId) {
- const previous = owner.items.get(itemEvent.itemId);
- if (!previous || itemEvent.revision > previous.revision) {
- owner.items.set(itemEvent.itemId, itemEvent);
- yield* publishItem(owner, itemEvent);
- }
- return;
- }
- }
- // A workflow child's approval comes under the child's own run id, during a turn
- // or after it, so it skips turn routing.
- if (params.subagentOrigin !== undefined && method.startsWith("approval/"))
- return yield* handleRequestEvent(method, params, undefined);
- // Muse starts a turn on its own when a workflow finishes, to report its result.
- // During a run, that run shows it. Otherwise hold it and ask the orchestrator for
- // a run; that run takes the held events, unless a user turn takes them first.
- if (
- method === "turn/started" &&
- typeof params.turnId === "string" &&
- params.turnId !== active?.nativeId &&
- params.turnId !== wake?.nativeId
- ) {
- if (active && !active.compact) active.joined.add(params.turnId);
- else if (!active && !wake) {
- const held = { nativeId: params.turnId, events: [] };
- wake = held;
- const notification = backgroundWorkNotification(finishedBackground.splice(0));
- if (thread?.appThreadId && options.continuationRequests)
- yield* options.continuationRequests.offer({
- threadId: thread.appThreadId,
- providerThreadId: thread.id,
- driver: MUSE_PROVIDER,
- detail: null,
- ...(notification ? { notification } : {}),
- dispatchIfCurrent: (dispatch) =>
- wake === held ? Effect.map(dispatch, Option.some) : Effect.succeed(Option.none()),
- clearIfCurrent: () =>
- Effect.sync(() => {
- if (wake === held) wake = undefined;
- }),
- });
- }
- }
- if (wake && active?.nativeId !== wake.nativeId) {
- const turnId = typeof params.turnId === "string" ? params.turnId : itemEvent?.turnId;
- if (turnId === wake.nativeId) {
- wake.events.push([method, data]);
- return;
- }
- }
- const turn = active;
- if (!turn) return;
- if (typeof params.turnId === "string" && !owns(turn, params.turnId) && !turn.compact)
- return;
- switch (method) {
- case "item/started":
- case "item/updated":
- case "item/completed": {
- const item = itemEvent!;
- if (
- item.turnId &&
- !owns(turn, item.turnId) &&
- !(turn.compact && item.kind === "compaction")
- )
- return;
- const previous = turn.items.get(item.itemId);
- if (previous && previous.revision >= item.revision) return;
- turn.items.set(item.itemId, item);
- turn.dirty.delete(item.itemId);
- yield* publishItem(turn, item);
- if (turn.compact && item.kind === "compaction" && item.status !== "inProgress") {
- yield* finish(
- turn,
- item.status === "completed" && (!item.outcome || item.outcome === "compacted")
- ? "completed"
- : item.status === "cancelled"
- ? "cancelled"
- : "failed",
- item.failureReason ?? item.reason,
- );
- }
- break;
- }
- case "item/delta": {
- const delta = yield* decode(MuseDelta, params);
- const previous = turn.items.get(delta.itemId);
- if (!previous || previous.status !== "inProgress") return;
- const field = delta.field ?? "text";
- let item = previous;
- if (field === "text") item = { ...previous, text: (previous.text ?? "") + delta.delta };
- else if (field === "output" || field === "visibleOutput")
- item = { ...previous, visibleOutput: (previous.visibleOutput ?? "") + delta.delta };
- else if (field.startsWith("summary.")) {
- const index = Number(field.slice(8));
- if (Number.isSafeInteger(index) && index >= 0 && index < 100) {
- const summary = [...(previous.summary ?? [])];
- summary[index] = (summary[index] ?? "") + delta.delta;
- item = { ...previous, summary };
- }
- }
- if (item === previous) return;
- turn.items.set(item.itemId, item);
- turn.dirty.add(item.itemId);
- if (!turn.flushScheduled) {
- turn.flushScheduled = true;
- yield* Effect.sleep(50).pipe(
- Effect.andThen(Queue.offer(inbox, { type: "flush", turn })),
- Effect.forkIn(scope),
- );
- }
- break;
- }
- case "turn/completed": {
- if (turn.compact) return;
- const result = yield* decode(MuseTurnCompleted, params);
- // A joined Muse turn ending does not end this run; its own turn does.
- if (result.turnId !== turn.nativeId) break;
- if (result.usage) {
- const usage = result.usage;
- turn.providerTurn = {
- ...turn.providerTurn,
- turnTokenUsage: {
- usageScope: "main_agent",
- usageStatus: "complete",
- hasSubagents: [...turn.items.values()].some((item) => item.kind === "subagent"),
- inputTokens: turn.providerTurn.turnTokenUsage?.inputTokens ?? usage.inputTokens,
- outputTokens: usage.outputTokens,
- cachedInputTokens: usage.cacheReadTokens ?? usage.cachedTokens,
- reasoningTokens: usage.reasoningTokens,
- ...(usage.cacheWriteTokens === undefined
- ? {}
- : { cacheCreationTokens: usage.cacheWriteTokens }),
- },
- };
- }
- const status =
- result.terminal === "completed"
- ? "completed"
- : result.terminal === "cancelled"
- ? turn.interruptRequested
- ? "interrupted"
- : "cancelled"
- : "failed";
- yield* finish(turn, status, result.error?.message ?? result.reason);
- break;
- }
- case "approval/requested":
- case "approval/updated":
- case "approval/resolved":
- case "userInput/requested":
- case "userInput/settled":
- yield* handleRequestEvent(method, params, turn);
- break;
- case "session/tokenUsage": {
- const usage = yield* decode(MuseTokenUsageEvent, params);
- const previous = turn.providerTurn.turnTokenUsage;
- const contextUsage = thread?.contextUsage;
- turn.providerTurn = {
- ...turn.providerTurn,
- ...(contextUsage
- ? {
- tokenUsage: {
- ...turn.providerTurn.tokenUsage,
- usedTokens: contextUsage.usedTokens,
- inputTokens: usage.usage.inputTokens,
- outputTokens: usage.usage.outputTokens,
- cachedInputTokens: usage.usage.cacheReadTokens ?? usage.usage.cachedTokens,
- reasoningOutputTokens: usage.usage.reasoningTokens,
- updatedAt: DateTime.formatIso(yield* DateTime.now),
- },
- }
- : {}),
- turnTokenUsage: {
- usageScope: "main_agent",
- usageStatus: "partial",
- hasSubagents: [...turn.items.values()].some((item) => item.kind === "subagent"),
- inputTokens: (previous?.inputTokens ?? 0) + usage.promptTokens,
- outputTokens: (previous?.outputTokens ?? 0) + usage.usage.outputTokens,
- cachedInputTokens:
- (previous?.cachedInputTokens ?? 0) +
- (usage.usage.cacheReadTokens ?? usage.usage.cachedTokens),
- ...(usage.usage.cacheWriteTokens === undefined &&
- previous?.cacheCreationTokens === undefined
- ? {}
- : {
- cacheCreationTokens:
- (previous?.cacheCreationTokens ?? 0) + (usage.usage.cacheWriteTokens ?? 0),
- }),
- reasoningTokens: (previous?.reasoningTokens ?? 0) + usage.usage.reasoningTokens,
- },
- };
- yield* emit({
- type: "provider_turn.updated",
- driver: MUSE_PROVIDER,
- providerTurn: turn.providerTurn,
- });
- break;
- }
- case "session/todoListChanged": {
- const todo = yield* decode(MuseTodoList, params);
- const steps: ReadonlyArray = todo.items
- .filter((item) => item.text.trim() && item.status !== "cancelled")
- .map((item, index) => ({
- id: `${turn.nativeId}:todo:${index}`,
- text: item.text.trim(),
- status:
- item.status === "completed"
- ? "completed"
- : item.status === "inProgress"
- ? "running"
- : "pending",
- }));
- const time = yield* DateTime.now;
- const base = baseItem(turn, "todo", time);
- const planId =
- turn.todoPlanId ??
- (yield* idAllocator.allocate
- .plan({
- threadId: turn.input.threadId,
- runId: turn.input.runId,
- driver: MUSE_PROVIDER,
- })
- .pipe(
- Effect.mapError((cause) =>
- protocolError("Cannot allocate Muse todo list", cause),
- ),
- ));
- turn.todoPlanId = planId;
- yield* emit({
- type: "plan.updated",
- driver: MUSE_PROVIDER,
- plan: {
- id: planId,
- threadId: turn.input.threadId,
- runId: turn.input.runId,
- nodeId: base.nodeId,
- kind: "todo_list",
- status: steps.every((step) => step.status === "completed") ? "completed" : "active",
- steps,
- },
- });
- yield* emit({
- type: "node.updated",
- driver: MUSE_PROVIDER,
- node: {
- id: base.nodeId,
- threadId: base.threadId,
- runId: base.runId,
- parentNodeId: turn.input.rootNodeId,
- rootNodeId: turn.input.rootNodeId,
- kind: "todo_list",
- status: "completed",
- countsForRun: false,
- providerThreadId: base.providerThreadId,
- providerTurnId: base.providerTurnId,
- nativeItemRef: null,
- runtimeRequestId: null,
- checkpointScopeId: null,
- startedAt: base.startedAt,
- completedAt: time,
- },
- });
- yield* emit({
- type: "turn_item.updated",
- driver: MUSE_PROVIDER,
- turnItem: {
- ...base,
- nativeItemRef: null,
- type: "todo_list",
- status: "completed",
- title: null,
- completedAt: time,
- planId,
- steps,
- },
- });
- break;
- }
- case "turn/retryScheduled": {
- const retry = yield* decode(MuseTurnRetryScheduled, params);
- const time = yield* DateTime.now;
- yield* emit({
- type: "turn_item.updated",
- driver: MUSE_PROVIDER,
- turnItem: {
- ...baseItem(turn, `retry:${retry.nextAttempt}`, time),
- type: "system_notice",
- status: "completed",
- title: "Muse retry",
- completedAt: time,
- message: `Muse is retrying this turn (attempt ${retry.nextAttempt} of ${retry.maxAttempts}): ${retry.reason}`,
- },
- });
- break;
- }
- }
- });
- yield* Stream.fromQueue(inbox).pipe(
- Stream.runForEach((entry) =>
- Effect.gen(function* () {
- if (entry.type === "flush") {
- const turn = entry.turn;
- turn.flushScheduled = false;
- if (active !== turn) return;
- for (const id of turn.dirty) {
- const item = turn.items.get(id);
- if (item) yield* publishItem(turn, item);
- }
- turn.dirty.clear();
- } else if (entry.epoch === hostEpoch) {
- if (entry.type === "failure") yield* failHost(entry.cause);
- else {
- if (options.nativeEventLogger && entry.method !== "item/delta")
- yield* options.nativeEventLogger.write(
- { provider: "muse", method: entry.method, params: entry.params },
- input.threadId,
- );
- yield* handleNotification(entry.method, entry.params).pipe(
- Effect.catch((cause) =>
- INFORMATIONAL_NOTIFICATIONS.has(entry.method)
- ? Effect.logWarning(`Skipped an unreadable Muse ${entry.method}`, cause)
- : failHost(cause),
- ),
- );
- }
- }
- }).pipe(eventPermit.withPermits(1)),
- ),
- Effect.forkIn(scope),
- );
- const launchHost = Effect.fnUntraced(function* () {
- const epoch = ++hostEpoch;
- const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
- const created = yield* Effect.acquireRelease(
- createMuseSdkHostEffect(
- {
- binaryPath: options.settings.binaryPath || "muse",
- cwd,
- environment: McpProviderSession.withAgentDeviceEnvironment(
- options.environment,
- mcpSession,
- ),
- runtimeMode: input.runtimePolicy.runtimeMode,
- },
- options.createHost,
- ).pipe(
- Effect.mapError(
- (error) =>
- new ProviderAdapterOpenSessionError({
- driver: MUSE_PROVIDER,
- providerSessionId: input.providerSessionId,
- cause: error.cause,
- }),
- ),
- ),
- (created) =>
- Effect.gen(function* () {
- if (host === created) {
- closed = true;
- hostEpoch++;
- }
- yield* Effect.tryPromise(() => created.close()).pipe(Effect.ignore);
- }),
- { interruptible: true },
- ).pipe(Effect.provideService(Scope.Scope, scope));
- host = created;
- created.connection.onNotification((notification) => {
- Queue.offerUnsafe(inbox, {
- type: "notification",
- method: notification.method,
- params: notification.params,
- epoch,
- });
- });
- created.connection.onProtocolError((cause) => {
- Queue.offerUnsafe(inbox, { type: "failure", cause, epoch });
- });
- // These are presentation receipts only. The matching notifications drive the UI,
- // and decisions travel as `approval/decide` and `userInput/answer` commands.
- created.connection.onServerRequest(async (request) => {
- if (request.method === "approval/request" || request.method === "userInput/request")
- return {};
- throw new Error(`Unsupported Muse server request: ${request.method}`);
- });
- void created.connection.closed.then(() => {
- if (!closed && epoch === hostEpoch)
- Queue.offerUnsafe(inbox, {
- type: "failure",
- cause: new Error("Muse connection closed unexpectedly"),
- epoch,
- });
- });
- void created.exited.then((exit) => {
- if (!closed && epoch === hostEpoch) {
- const reason = created
- .stderrTail?.()
- .findLast((line) => line.trim())
- ?.trim();
- Queue.offerUnsafe(inbox, {
- type: "failure",
- cause: new Error(
- `Muse exited unexpectedly (${exit.code ?? exit.signal})${reason ? `: ${reason}` : ""}`,
- ),
- epoch,
- });
- }
- });
- });
- yield* Effect.addFinalizer(() =>
- Effect.gen(function* () {
- closed = true;
- hostEpoch++;
- if (active) yield* finish(active, "cancelled", "Muse session closed", "broken");
- yield* settleObservedChildren("cancelled");
- yield* Queue.shutdown(inbox);
- yield* Queue.shutdown(events);
- }).pipe(eventPermit.withPermits(1)),
- );
- yield* launchHost();
-
- const register = Effect.fnUntraced(function* (args: ProviderAdapterV2EnsureThreadInput) {
- if (broken || closed)
- return yield* protocolError("Muse host is unavailable; reopen the session");
- if (active)
- return yield* protocolError("Cannot change Muse sessions during an active turn");
- const existing = args.existingProviderThread;
- if (
- existing &&
- (existing.driver !== MUSE_PROVIDER || existing.providerInstanceId !== options.instanceId)
- )
- return yield* protocolError("Muse thread belongs to another provider instance");
- const requestedId = existing
- ? (existing.nativeThreadRef?.nativeId ?? undefined)
- : input.initialNativeThreadId;
- if (
- thread &&
- (!requestedId || requestedId === nativeSessionId) &&
- thread.appThreadId === args.threadId
- )
- return thread;
- if (thread) return yield* protocolError("Open a new host to attach another Muse session");
- let missingNativeSession = false;
- return yield* Effect.gen(function* () {
- nativeSessionId = requestedId ?? host.connection.mintCommandId();
- const mcpSession = McpProviderSession.readMcpProviderSession(args.threadId);
- if (mcpSession && !host.initializeResult.grantedCapabilities.includes("sessionMcp"))
- return yield* protocolError(
- "Update Muse Code to a version that supports session MCP servers",
- );
- const config = mcpSession
- ? {
- mcpServers: {
- // Muse defaults to "required", which fails the whole run when T3's
- // tools cannot be reached. The agent should still work without them.
- "t3-code": {
- transport: "streamableHttp",
- mode: "optional",
- url: mcpSession.endpoint,
- headers: { Authorization: mcpSession.authorizationHeader },
- },
- },
- }
- : undefined;
- const startModel = yield* resolveModel(args.modelSelection);
- const result = yield* request(
- requestedId ? "session/resume" : "session/start",
- requestedId
- ? // T3 already has the transcript; skip Muse's history payload.
- { excludeItems: true, ...(config ? { config } : {}) }
- : {
- workspaceRoot: cwd,
- ...(config ? { config } : {}),
- ...(startModel ? { modelId: startModel } : {}),
- providerId: "meta",
- approvalMode: museApprovalMode(args.runtimePolicy.runtimeMode),
- },
- ).pipe(
- Effect.catch((error) => {
- missingNativeSession =
- requestedId !== undefined &&
- error.payload instanceof MspError &&
- (error.payload.kind === "sessionNotFound" || error.payload.kind === "notFound");
- return Effect.fail(error);
- }),
- Effect.flatMap((value) => decode(MuseSessionResult, value)),
- );
- if (result.session.sessionId !== nativeSessionId)
- return yield* protocolError("Muse returned a different session identity");
- if (requestedId)
- yield* request("session/setApprovalMode", {
- mode: museApprovalMode(args.runtimePolicy.runtimeMode),
- });
- // A resumed session keeps the model it last ran with; the next turn
- // switches it when the selection differs.
- const createdAt = yield* DateTime.now;
- thread = existing
- ? {
- ...existing,
- providerSessionId: input.providerSessionId,
- nativeThreadRef: nativeRef(result.session.sessionId),
- status: "idle",
- updatedAt: createdAt,
- }
- : {
- id: idAllocator.derive.providerThread({
- driver: MUSE_PROVIDER,
- providerInstanceId: options.instanceId,
- nativeThreadId: nativeSessionId,
- }),
- driver: MUSE_PROVIDER,
- providerInstanceId: options.instanceId,
- providerSessionId: input.providerSessionId,
- appThreadId: args.threadId,
- ownerNodeId: null,
- nativeThreadRef: nativeRef(nativeSessionId),
- nativeConversationHeadRef: null,
- status: "idle",
- firstRunOrdinal: null,
- lastRunOrdinal: null,
- handoffIds: [],
- forkedFrom: null,
- createdAt,
- updatedAt: createdAt,
- };
- session = {
- ...session,
- model: result.session.modelId ?? args.modelSelection.model,
- };
- yield* emit({
- type: "provider_thread.updated",
- driver: MUSE_PROVIDER,
- providerThread: thread,
- });
- return thread;
- }).pipe(
- Effect.onError((cause) =>
- missingNativeSession ? Effect.void : eventPermit.withPermits(1)(failHost(cause)),
- ),
- );
- });
- const prompt = Effect.fnUntraced(function* (message: ProviderAdapterV2TurnMessage) {
- const parts: SendUserTurnOptions["input"] = [];
- const text = providerMessageTextWithAttachmentPaths({
- text: message.text,
- attachments: message.attachments,
- resolveAttachmentPath: (attachment) =>
- resolveAttachmentPath({
- attachmentsDir: options.serverConfig.attachmentsDir,
- attachment,
- }),
- });
- if (text) parts.push({ type: "text", text });
- for (const attachment of message.attachments)
- if (isProviderNativeImageAttachment(attachment)) {
- const path = resolveAttachmentPath({
- attachmentsDir: options.serverConfig.attachmentsDir,
- attachment,
- });
- if (!path) return yield* protocolError("Muse image attachment is missing");
- const bytes = yield* options.fileSystem
- .readFile(path)
- .pipe(Effect.mapError((cause) => protocolError("Cannot read Muse image", cause)));
- parts.push({
- type: "image",
- base64Data: Buffer.from(bytes).toString("base64"),
- mediaType: attachment.mimeType,
- });
- }
- if (!parts.length) return yield* protocolError("Muse needs text or an image");
- return parts;
- });
- const validateThread = (candidate: OrchestrationV2ProviderThread) =>
- thread !== undefined &&
- candidate.driver === MUSE_PROVIDER &&
- candidate.providerInstanceId === options.instanceId &&
- candidate.appThreadId === thread.appThreadId &&
- candidate.nativeThreadRef?.nativeId === nativeSessionId;
- const selectionEffort = Effect.fnUntraced(function* (selection: ModelSelection) {
- if (selection.instanceId !== options.instanceId)
- return yield* protocolError("Model selection belongs to another Muse instance");
- const catalog = options.modelCatalog ? yield* options.modelCatalog : [];
- const model = yield* resolveModel(selection);
- const selected = getModelSelectionStringOptionValue(selection, "reasoningEffort");
- // With no saved choice, the model's own default applies.
- return resolveMuseReasoningEffort(
- catalog.find((entry) => entry.slug === model)?.capabilities ?? museModelCapabilities(),
- selected,
- );
- });
- const start = Effect.fnUntraced(function* (
- turnInput: ProviderAdapterV2TurnInput,
- compact = false,
- ) {
- if (!validateThread(turnInput.providerThread) || broken || closed)
- return yield* protocolError("Muse thread is not attached to this host");
- if (active)
- return yield* protocolError(
- "Muse already has an active turn; queue through the orchestrator",
- );
- if (turnInput.runtimePolicy.interactionMode === "plan")
- return yield* protocolError("Muse does not support dedicated Plan mode");
- if (turnInput.runtimePolicy.runtimeMode !== input.runtimePolicy.runtimeMode)
- return yield* protocolError("Runtime policy changes require a fresh Muse host");
- // A continuation run takes the turn Muse already started; it sends no prompt.
- const continuation =
- turnInput.message.createdBy === "agent" &&
- turnInput.message.creationSource === "provider";
- const adopted = continuation ? wake : undefined;
- const effort = continuation ? undefined : yield* selectionEffort(turnInput.modelSelection);
- const parts = compact || continuation ? [] : yield* prompt(turnInput.message);
- const targetModel = continuation
- ? undefined
- : yield* resolveModel(turnInput.modelSelection);
- if (targetModel && session.model !== targetModel) {
- yield* request("session/setModel", {
- model: { modelId: targetModel, providerId: "meta" },
- });
- session = { ...session, model: targetModel };
- }
- const nativeId = adopted?.nativeId ?? host.connection.mintCommandId();
- const startedAt = yield* DateTime.now;
- const providerTurn: OrchestrationV2ProviderTurn = {
- id: idAllocator.derive.providerTurn({
- driver: MUSE_PROVIDER,
- nativeTurnId: `${options.instanceId}:${nativeSessionId}:${nativeId}`,
- }),
- providerThreadId: turnInput.providerThread.id,
- nodeId: turnInput.rootNodeId,
- runAttemptId: turnInput.attemptId,
- nativeTurnRef: compact
- ? { ...nativeRef(nativeId), strength: "weak" }
- : nativeRef(nativeId),
- ordinal: turnInput.providerTurnOrdinal,
- status: "running",
- startedAt,
- completedAt: null,
- };
- const turn: ActiveTurn = {
- input: turnInput,
- providerTurn,
- nativeId,
- joined: new Set(),
- items: new Map(),
- ordinals: new Map(),
- started: new Map(),
- dirty: new Set(),
- settledRequests: new Set(),
- done: yield* Deferred.make(),
- nextOrdinal: turnInput.providerTurnOrdinal * 100 + 1,
- flushScheduled: false,
- interruptRequested: false,
- compact,
- };
- yield* Effect.gen(function* () {
- thread = turnInput.providerThread;
- active = turn;
- yield* emit({
- type: "provider_turn.updated",
- driver: MUSE_PROVIDER,
- threadId: turnInput.threadId,
- providerTurn,
- });
- yield* updateThread({
- status: "active",
- firstRunOrdinal: thread.firstRunOrdinal ?? turnInput.runOrdinal,
- lastRunOrdinal: turnInput.runOrdinal,
- });
- yield* updateSession("running");
- // A user turn takes a held Muse turn: Muse runs it first, so this run shows it
- // (and its approvals), and the continuation asked for it is no longer needed.
- const held = wake;
- wake = undefined;
- if (continuation && !held) return yield* finish(turn, "completed");
- if (held && !continuation) turn.joined.add(held.nativeId);
- for (const [method, data] of held?.events ?? [])
- yield* handleNotification(method, data).pipe(Effect.catch(failHost));
- }).pipe(eventPermit.withPermits(1));
- if (continuation) return;
- yield* Effect.gen(function* () {
- if (compact) {
- const result = yield* request("session/compact", {}, true, nativeId).pipe(
- Effect.flatMap((value) => decode(MuseCompactResult, value)),
- );
- if (result.status === "noop")
- yield* eventPermit.withPermits(1)(
- finish(turn, "failed", result.reason ?? "Muse has no context to compact."),
- );
- else if (result.status !== "accepted")
- return yield* protocolError(
- `Muse returned an unsupported compaction status: ${result.status}`,
- );
- } else {
- const result = yield* request(
- "turn/start",
- {
- input: [
- {
- type: "text",
- text: buildRuntimeInstructions({
- harness: "Muse Code",
- model: session.model ?? undefined,
- reasoningEffort: effort,
- }),
- },
- ...parts,
- ],
- displayText: turnInput.message.text || "Image attachment",
- // A resumed session keeps the root it was created with; pin it to
- // this thread's current checkout so edits land where T3 tracks them.
- workspaceRoots: [cwd],
- ifBusy: "queue",
- ...(effort ? { reasoningEffort: effort } : {}),
- },
- true,
- nativeId,
- ).pipe(Effect.flatMap((value) => decode(MuseTurnStartResult, value)));
- if (result.turnId !== nativeId) {
- yield* eventPermit.withPermits(1)(
- failHost(new Error("Muse admitted an unowned turn")),
- );
- return yield* protocolError("Muse admitted the turn under an unexpected identity");
- }
- }
- }).pipe(
- Effect.onError((cause) => {
- const error = Cause.squash(cause);
- const detail = isProtocolError(error)
- ? error.detail
- : error instanceof Error
- ? error.message
- : "Muse could not start the turn.";
- return eventPermit.withPermits(1)(finish(turn, "failed", detail));
- }),
- );
- });
- // Only messages are read back; T3 owns turn and item history.
- const snapshot = Effect.fnUntraced(function* (
- current: OrchestrationV2ProviderThread,
- ): Effect.fn.Return {
- const result = yield* request("session/read", { excludeItems: false }, false).pipe(
- Effect.flatMap((value) => decode(MuseSessionResult, value)),
- );
- if (result.session.sessionId !== nativeSessionId)
- return yield* protocolError("Muse snapshot belongs to a different session");
- const items = result.history?.items ?? result.history?.snapshot?.state.items ?? [];
- const now = yield* DateTime.now;
- return {
- providerThread: current,
- providerTurns: [],
- messages: items.flatMap((item): OrchestrationV2ConversationMessage[] =>
- item.kind === "agentMessage" || item.kind === "userMessage"
- ? [
- {
- id: idAllocator.derive.messageFromProviderItem({
- driver: MUSE_PROVIDER,
- nativeItemId: `${options.instanceId}:${current.id}:${item.turnId ?? "history"}:${item.itemId}`,
- }),
- threadId: current.appThreadId ?? input.threadId,
- runId: null,
- nodeId: null,
- role: item.kind === "agentMessage" ? "assistant" : "user",
- text: item.text ?? "",
- attachments: [],
- streaming: item.status === "inProgress",
- createdBy: item.kind === "agentMessage" ? "agent" : "user",
- creationSource: "provider",
- createdAt: now,
- updatedAt: now,
- },
- ]
- : [],
- ),
- runtimeRequests: [...pending.values()].map((entry) => entry.request),
- providerPayload: items,
- };
- });
- const runtime: ProviderAdapterV2SessionRuntime = {
- instanceId: options.instanceId,
- driver: MUSE_PROVIDER,
- providerSessionId: input.providerSessionId,
- providerSession: session,
- events: Stream.fromQueue(events),
- // A running workflow or a held Muse turn keeps the host: idle release must wait.
- hasPendingBackgroundWork: Effect.sync(
- () => observedChildren.size > 0 || wake !== undefined,
- ),
- ensureThread: (args) =>
- register(args).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterEnsureThreadError({
- driver: MUSE_PROVIDER,
- threadId: args.threadId,
- cause,
- }),
- ),
- ),
- resumeThread: (args) =>
- register({
- threadId: args.threadId ?? args.providerThread.appThreadId ?? input.threadId,
- modelSelection: args.modelSelection ?? input.modelSelection,
- runtimePolicy: args.runtimePolicy ?? input.runtimePolicy,
- existingProviderThread: args.providerThread,
- }).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterResumeThreadError({
- driver: MUSE_PROVIDER,
- providerSessionId: input.providerSessionId,
- providerThreadId: args.providerThread.id,
- cause,
- }),
- ),
- ),
- startTurn: (args) =>
- start(args).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterTurnStartError({
- driver: MUSE_PROVIDER,
- threadId: args.threadId,
- providerThreadId: args.providerThread.id,
- runId: args.runId,
- cause,
- }),
- ),
- ),
- compactThread: (args) => start(args, true).pipe(commands.withPermits(1)),
- steerTurn: (args) =>
- Effect.gen(function* () {
- const turn = active;
- if (
- !turn ||
- turn.providerTurn.id !== args.providerTurnId ||
- !validateThread(args.providerThread) ||
- turn.compact
- )
- return yield* protocolError("This Muse turn cannot be steered");
- const parts = yield* prompt(args.message);
- const result = yield* request("turn/steer", {
- expectedTurnId: turn.nativeId,
- input: parts,
- }).pipe(Effect.flatMap((value) => decode(MuseTurnStartResult, value)));
- if (result.turnId !== turn.nativeId)
- return yield* protocolError("Muse steering targeted a different turn");
- }).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterSteerRunError({
- driver: MUSE_PROVIDER,
- providerThreadId: args.providerThread.id,
- providerTurnId: args.providerTurnId,
- cause,
- }),
- ),
- ),
- interruptTurn: (args) =>
- Effect.gen(function* () {
- const turn = active;
- if (
- !turn ||
- turn.providerTurn.id !== args.providerTurnId ||
- !validateThread(args.providerThread)
- )
- return yield* protocolError("This Muse turn is no longer active");
- turn.interruptRequested = true;
- if (turn.compact) {
- yield* eventPermit.withPermits(1)(
- Effect.gen(function* () {
- if (active !== turn) return;
- hostEpoch++;
- yield* Effect.tryPromise(() => host.close()).pipe(
- Effect.mapError((cause) => protocolError("Cannot stop Muse compaction", cause)),
- Effect.onError(failHost),
- );
- broken = true;
- yield* finish(turn, "interrupted", undefined, "broken");
- yield* settleObservedChildren("interrupted");
- yield* updateSession("stopped");
- yield* Queue.end(events);
- }),
- );
- return;
- }
- // Muse runs a joined turn first, so Stop must end it too.
- for (const joined of turn.joined)
- yield* request("turn/interrupt", { turnId: joined }).pipe(Effect.ignore);
- yield* request("turn/interrupt", { turnId: turn.nativeId });
- yield* Deferred.await(turn.done).pipe(
- Effect.timeout(options.requestTimeoutMs ?? 30_000),
- Effect.mapError((cause) => protocolError("Muse did not confirm interruption", cause)),
- Effect.onError((cause) => eventPermit.withPermits(1)(failHost(cause))),
- );
- }).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterInterruptError({
- driver: MUSE_PROVIDER,
- providerThreadId: args.providerThread.id,
- providerTurnId: args.providerTurnId,
- cause,
- }),
- ),
- ),
- respondToRuntimeRequest: (args) =>
- Effect.gen(function* () {
- const entry = pending.get(args.requestId);
- if (!entry) return yield* protocolError("This Muse request is no longer pending");
- if (entry.native.type === "approval") yield* decideApproval(entry, args.decision);
- else {
- const questionRequest = entry.native.value;
- const answers = [];
- for (const question of questionRequest.questions) {
- const raw = yield* decode(responseAnswerSchema, args.answers?.[question.id]);
- const values = typeof raw === "string" ? [raw] : [...new Set(raw)];
- const selected = values.filter((value) =>
- question.options.some((option) => option.label === value),
- );
- const freeText = values.filter((value) => !selected.includes(value)).join("\n");
- if (freeText.length > 500 || (!freeText && !selected.length))
- return yield* protocolError(
- "Muse needs a nonempty answer with at most 500 custom characters",
- );
- if (
- (question.selection.mode === "single" && selected.length > 1) ||
- (selected.length > 0 &&
- (selected.length < (question.selection.minSelections ?? 0) ||
- selected.length > (question.selection.maxSelections ?? Infinity)))
- )
- return yield* protocolError("Muse question selection count is invalid");
- answers.push({
- questionId: question.id,
- ...(selected.length
- ? question.selection.mode === "single"
- ? { selectedLabel: selected[0]! }
- : { selectedLabels: selected }
- : {}),
- ...(freeText ? (selected.length ? { note: freeText } : { freeText }) : {}),
- });
- }
- if (args.answers) entry.response = { answers: args.answers };
- yield* request("userInput/answer", {
- userInputId: questionRequest.userInputId,
- answers,
- }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response)));
- }
- }).pipe(
- Effect.mapError(
- (cause) =>
- new ProviderAdapterRuntimeRequestResponseError({
- driver: MUSE_PROVIDER,
- requestId: args.requestId,
- cause,
- }),
- ),
- ),
- readThreadSnapshot: (args) =>
- (!validateThread(args.providerThread)
- ? protocolError("Snapshot requested for a different Muse thread")
- : snapshot(args.providerThread)
- ).pipe(
- commands.withPermits(1),
- Effect.mapError(
- (cause) =>
- new ProviderAdapterReadThreadSnapshotError({
- driver: MUSE_PROVIDER,
- providerThreadId: args.providerThread.id,
- cause,
- }),
- ),
- ),
- // Muse rejects forks at completed-turn boundaries (InvalidCut), so T3 uses
- // portable context handoff for forks and does not rewind Muse conversations.
- rollbackThread: (args) =>
- Effect.fail(
- new ProviderAdapterRollbackThreadError({
- driver: MUSE_PROVIDER,
- providerThreadId: args.providerThread.id,
- checkpointId: args.target.checkpointId,
- cause: "Muse Code does not support conversation rollback in T3 Code.",
- }),
- ),
- forkThread: (args) =>
- Effect.fail(
- new ProviderAdapterForkThreadError({
- driver: MUSE_PROVIDER,
- providerThreadId: args.sourceProviderThread.id,
- cause: "Muse Code does not support native forks in T3 Code.",
- }),
- ),
- };
- return runtime;
- }),
- });
-}
diff --git a/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.test.ts b/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.test.ts
deleted file mode 100644
index 7e4f489b5b00994398a3fcc2f2103f5bc55a4b7f..0000000000000000000000000000000000000000
--- a/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.test.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { describe, expect, it } from "@effect/vitest";
-
-import type { MuseItem } from "../../provider/museProtocol.ts";
-import { museItemStatus, museToolPresentation } from "./MuseItemPresentation.ts";
-
-const item = (fields: Partial): MuseItem => ({
- itemId: "native-item",
- kind: "toolCall",
- status: "completed",
- revision: 1,
- ...fields,
-});
-
-describe("Muse item presentation", () => {
- it("preserves an edit's file and exact old/new text for the shared diff UI", () => {
- expect(
- museToolPresentation(
- item({
- tool: "edit_file",
- args: JSON.stringify({ file_path: "src/index.ts", old_string: "old\n", new_string: "" }),
- }),
- ),
- ).toMatchObject({
- type: "file_change",
- status: "completed",
- fileName: "src/index.ts",
- oldStr: "old\n",
- newStr: "",
- });
- });
-
- it("preserves write content and provider-supplied patches", () => {
- expect(
- museToolPresentation(
- item({
- tool: "write_file",
- args: JSON.stringify({ path: "README.md", content: "Hello\n" }),
- }),
- ),
- ).toMatchObject({ type: "file_change", newStr: "Hello\n" });
- expect(
- museToolPresentation(
- item({
- tool: "apply_patch",
- args: JSON.stringify({ path: "README.md", patch: "@@ -1 +1 @@\n-old\n+new" }),
- }),
- ),
- ).toMatchObject({ type: "file_change", diffStr: "@@ -1 +1 @@\n-old\n+new" });
- });
-
- it.each(["rejected", "timedOut", "handedOff"])(
- "does not report native %s activity as successful",
- (status) => {
- const native = item({ tool: "search_files", status, fallbackText: "Native explanation" });
- expect(museItemStatus(native)).toBe("failed");
- expect(museToolPresentation(native)).toMatchObject({
- status: "failed",
- output: `Muse reported ${status}.\nNative explanation`,
- });
- },
- );
-
- it("preserves failure details and a known cancelled status", () => {
- expect(
- museToolPresentation(
- item({
- status: "failed",
- failureReason: "Permission denied",
- visibleOutput: "Partial output",
- }),
- ),
- ).toMatchObject({ status: "failed", output: "Permission denied\nPartial output" });
- expect(museItemStatus(item({ status: "cancelled" }))).toBe("cancelled");
- });
-
- it.each(["failed", "noop"])(
- "keeps %s compaction outcomes from appearing completed",
- (outcome) => {
- expect(museItemStatus(item({ kind: "compaction", outcome }))).toBe("failed");
- },
- );
-});
diff --git a/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.ts b/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.ts
deleted file mode 100644
index 93df7761a3ebd7599f564605a84278ea6edbdd9a..0000000000000000000000000000000000000000
--- a/apps/server/src/orchestration-v2/Adapters/MuseItemPresentation.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import type { OrchestrationV2TurnItem, OrchestrationV2TurnItemStatus } from "@t3tools/contracts";
-import * as Schema from "effect/Schema";
-
-import type { MuseItem } from "../../provider/museProtocol.ts";
-
-const decodeToolArgs = Schema.decodeUnknownSync(
- Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)),
-);
-
-type Fields<
- T extends OrchestrationV2TurnItem["type"],
- K extends keyof Extract,
-> = Pick, K>;
-type MuseToolPresentation = {
- readonly status: OrchestrationV2TurnItemStatus;
- readonly title: string | null;
-} & (
- | Fields<"command_execution", "type" | "input" | "output" | "exitCode">
- | Fields<"file_change", "type" | "fileName" | "oldStr" | "newStr" | "diffStr">
- | Fields<"web_search", "type" | "patterns" | "results">
- | Fields<"dynamic_tool", "type" | "toolName" | "input" | "output">
-);
-
-/** Unrecognized native terminal states must not appear as successful work. */
-export function museItemStatus(item: MuseItem): OrchestrationV2TurnItemStatus {
- if (item.status === "inProgress") return "running";
- if (item.status === "cancelled") return "cancelled";
- if (
- item.status === "completed" &&
- !(item.kind === "compaction" && item.outcome && item.outcome !== "compacted")
- )
- return "completed";
- return "failed";
-}
-
-/** Converts native tool details into the shared timeline fields used by every client. */
-export function museToolPresentation(
- item: MuseItem,
- status = museItemStatus(item),
-): MuseToolPresentation {
- let args: Record | undefined;
- if (item.args) {
- try {
- args = decodeToolArgs(item.args);
- } catch {
- // Streaming model-authored arguments may not be valid JSON yet.
- }
- }
- const stringArg = (...keys: string[]) => {
- for (const key of keys) {
- const value = args?.[key];
- if (typeof value === "string") return value;
- }
- return undefined;
- };
- const toolName = item.tool ?? item.kind;
- const title =
- item.kind === "subagent"
- ? item.objective || item.role || "Muse agent"
- : item.kind === "workflow"
- ? item.entryId || "Workflow"
- : toolName;
- const detail =
- status === "failed" ? item.failureReason || `Muse reported ${item.status}.` : undefined;
- const shared = { status, title: detail ? `${title}: ${detail}` : title };
- const text =
- item.visibleOutput ??
- item.result?.summary ??
- item.text ??
- item.summary?.join("\n") ??
- item.fallbackText ??
- item.objective ??
- "";
- const output = detail ? [detail, text].filter(Boolean).join("\n") : text;
-
- if (item.kind === "userShell" || /shell|bash|exec_command/i.test(item.tool ?? "")) {
- return {
- ...shared,
- type: "command_execution",
- input: item.commandText ?? stringArg("command", "cmd") ?? item.args ?? "",
- output,
- ...(item.exitCode === undefined ? {} : { exitCode: item.exitCode }),
- };
- }
- if (item.kind === "toolCall" && /write|edit|patch/i.test(toolName)) {
- const fileName = stringArg("file_path", "path", "filePath")?.trim();
- if (fileName) {
- const oldStr = stringArg("old_string", "old_str", "oldStr", "oldText");
- const newStr = stringArg("new_string", "new_str", "newStr", "newText", "content");
- const diffStr = stringArg("diff", "patch", "diffStr");
- return {
- ...shared,
- type: "file_change",
- fileName,
- ...(oldStr === undefined ? {} : { oldStr }),
- ...(newStr === undefined ? {} : { newStr }),
- ...(diffStr === undefined ? {} : { diffStr }),
- };
- }
- }
- if (item.kind === "toolCall" && /web|browse|fetch/i.test(toolName)) {
- const query = stringArg("query", "url", "pattern")?.trim();
- return {
- ...shared,
- type: "web_search",
- ...(query ? { patterns: [query] } : {}),
- ...(output ? { results: [{ snippet: output }] } : {}),
- };
- }
- return {
- ...shared,
- type: "dynamic_tool",
- toolName,
- input: args ?? item.args ?? "",
- output,
- };
-}
diff --git a/apps/server/src/provider/Drivers/MuseDriver.ts b/apps/server/src/provider/Drivers/MuseDriver.ts
deleted file mode 100644
index 16bed1c3dab27181b34f674e2050c9fbf02733c5..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/Drivers/MuseDriver.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { MuseSettings, ProviderDriverKind } from "@t3tools/contracts";
-import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Path from "effect/Path";
-import * as Schema from "effect/Schema";
-import { HttpClient } from "effect/http";
-import { ChildProcessSpawner } from "effect/process";
-
-import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost";
-import * as ServerConfig from "../../config.ts";
-import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
-import { makeMuseTextGeneration } from "../../textGeneration/MuseTextGeneration.ts";
-import { ProviderDriverError } from "../Errors.ts";
-import { makeMuseAdapterV2 } from "../../orchestration-v2/Adapters/MuseAdapterV2.ts";
-import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
-import { checkMuseProviderStatus, makePendingMuseProvider } from "../MuseProvider.ts";
-import * as ProviderEventLoggers from "../ProviderEventLoggers.ts";
-import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
-import { enrichMuseSnapshot, latestMuseVersion, museMaintenance } from "../museMaintenance.ts";
-import { makeMuseEnvironment } from "../museSdk.ts";
-import {
- defaultProviderContinuationIdentity,
- type ProviderDriver,
- type ProviderInstance,
-} from "@t3tools/provider-core/server/driver";
-import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
-import {
- makeCachedProviderMaintenanceResolution,
- resolveProviderMaintenanceCapabilitiesEffect,
-} from "@t3tools/provider-core/server/maintenanceResolver";
-import {
- haveProviderSnapshotSettingsChanged,
- makeProviderSnapshotSettingsSource,
- type ProviderSnapshotSettings,
-} from "@t3tools/provider-core/server/snapshotSettings";
-import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
-
-const DRIVER_KIND = ProviderDriverKind.make("muse");
-const decodeMuseSettings = Schema.decodeSync(MuseSettings);
-
-export type MuseDriverEnv =
- | IdAllocator.IdAllocatorV2
- | ProviderHost
- | ChildProcessSpawner.ChildProcessSpawner
- | FileSystem.FileSystem
- | HttpClient.HttpClient
- | Path.Path
- | ProviderEventLoggers.ProviderEventLoggers
- | ServerConfig.ServerConfig;
-
-export const MuseDriver: ProviderDriver = {
- driverKind: DRIVER_KIND,
- metadata: { displayName: "Muse Code", supportsMultipleInstances: true },
- configSchema: MuseSettings,
- defaultConfig: () => decodeMuseSettings({}),
- create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
- Effect.gen(function* () {
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const fileSystem = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const httpClient = yield* HttpClient.HttpClient;
- const host = yield* ProviderHost;
- const eventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
- const serverConfig = yield* ServerConfig.ServerConfig;
- const { cwd } = serverConfig;
- const idAllocator = yield* IdAllocator.IdAllocatorV2;
- const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
- const hostEnvironment = yield* HostProcessEnvironment;
- // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins.
- const processEnvironment = mergeProviderInstanceEnvironment(
- environment,
- makeMuseEnvironment(hostEnvironment),
- );
- const effectiveConfig = {
- ...config,
- enabled,
- binaryPath: expandHomePath(config.binaryPath),
- } satisfies MuseSettings;
- const continuationIdentity = defaultProviderContinuationIdentity({
- driverKind: DRIVER_KIND,
- instanceId,
- });
- const stampIdentity = withInstanceIdentity({
- instanceId,
- driverKind: DRIVER_KIND,
- displayName,
- accentColor,
- continuationGroupKey: continuationIdentity.continuationKey,
- });
- const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, host.settings);
- const resolveInstallation = yield* makeCachedProviderMaintenanceResolution(
- resolveProviderMaintenanceCapabilitiesEffect(museMaintenance, {
- binaryPath: effectiveConfig.binaryPath,
- env: processEnvironment,
- }).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- Effect.provideService(FileSystem.FileSystem, fileSystem),
- Effect.provideService(Path.Path, path),
- ),
- );
- const resolveMaintenance = (options?: { readonly fresh?: boolean }) =>
- Effect.gen(function* () {
- const capabilities = yield* resolveInstallation(options);
- // The maintenance runner requests fresh capabilities around an explicit update
- // and needs the native target version to verify that the command actually upgraded.
- const latestVersion = options?.fresh
- ? yield* latestMuseVersion(processEnvironment, { fresh: true }).pipe(
- Effect.provideService(HttpClient.HttpClient, httpClient),
- )
- : undefined;
- return latestVersion !== undefined ? { ...capabilities, latestVersion } : capabilities;
- });
- const snapshot = yield* makeManagedServerProvider>({
- resolveMaintenance,
- getSettings: snapshotSettings.getSettings,
- streamSettings: snapshotSettings.streamSettings,
- haveSettingsChanged: haveProviderSnapshotSettingsChanged,
- initialSnapshot: (settings) =>
- makePendingMuseProvider(settings.provider).pipe(Effect.map(stampIdentity)),
- checkProvider: checkMuseProviderStatus(effectiveConfig, processEnvironment, cwd).pipe(
- Effect.map(stampIdentity),
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- Effect.provideService(FileSystem.FileSystem, fileSystem),
- Effect.provideService(Path.Path, path),
- ),
- enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
- resolveMaintenance().pipe(
- Effect.flatMap((maintenanceCapabilities) =>
- enrichMuseSnapshot({
- snapshot: currentSnapshot,
- maintenanceCapabilities,
- enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
- environment: processEnvironment,
- }),
- ),
- Effect.provideService(HttpClient.HttpClient, httpClient),
- Effect.flatMap(publishSnapshot),
- ),
- }).pipe(
- Effect.mapError(
- (cause) =>
- new ProviderDriverError({
- driver: DRIVER_KIND,
- instanceId,
- detail: "Failed to build Muse Code snapshot.",
- cause,
- }),
- ),
- );
- const modelCatalog = snapshot.getSnapshot.pipe(Effect.map((current) => current.models));
- const orchestrationAdapter = makeMuseAdapterV2({
- instanceId,
- settings: effectiveConfig,
- environment: processEnvironment,
- idAllocator,
- serverConfig,
- fileSystem,
- modelCatalog,
- ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
- continuationRequests,
- });
- const textGeneration = yield* makeMuseTextGeneration(effectiveConfig, {
- environment: processEnvironment,
- modelCatalog,
- });
- return {
- instanceId,
- driverKind: DRIVER_KIND,
- continuationIdentity,
- displayName,
- accentColor,
- enabled,
- snapshot,
- orchestrationAdapter,
- textGeneration,
- } satisfies ProviderInstance;
- }),
-};
diff --git a/apps/server/src/provider/GrokProvider.test.ts b/apps/server/src/provider/GrokProvider.test.ts
index f77ee10465e292d6cc9a2311059c2fdb614faada..000176e9bb2318e68142cd72a5f5ec954ab9657c 100644
--- a/apps/server/src/provider/GrokProvider.test.ts
+++ b/apps/server/src/provider/GrokProvider.test.ts
@@ -18,7 +18,7 @@ import {
grokSlashCommandsFromInitialize,
parseGrokModelsCliOutput,
} from "./GrokProvider.ts";
-import { execScriptSource, writeFakeCli } from "../testUtils/fakeCli.ts";
+import { execScriptSource, writeFakeCli } from "@t3tools/provider-testing/fakeCli";
import { grokUsageResponseToLimits, readGrokAccount } from "./grokUsageLimits.ts";
const decodeGrokSettings = Schema.decodeSync(GrokSettings);
diff --git a/apps/server/src/provider/MuseProvider.test.ts b/apps/server/src/provider/MuseProvider.test.ts
deleted file mode 100644
index be7cafdbd6a152b16c0e05bb184eb76d167f042d..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/MuseProvider.test.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { expect, it } from "@effect/vitest";
-import { MuseSettings } from "@t3tools/contracts";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Schema from "effect/Schema";
-import { vi } from "vite-plus/test";
-
-import { writeFakeCli } from "../testUtils/fakeCli.ts";
-import type { MuseSdkHost } from "./museSdk.ts";
-import { COMPACT_SLASH_COMMAND } from "@t3tools/provider-core/server/snapshotProbe";
-import { checkMuseProviderStatus } from "./MuseProvider.ts";
-
-const settings = Schema.decodeSync(MuseSettings);
-const makeHost = (catalog: Record) => {
- const host: MuseSdkHost = {
- initializeResult: { grantedCapabilities: [] },
- connection: {
- request: vi.fn(async () => catalog),
- command: vi.fn(async () => ({})),
- mintCommandId: () => "test-command",
- onNotification: () => {},
- onServerRequest: () => {},
- onProtocolError: () => {},
- closed: new Promise(() => {}),
- },
- exited: new Promise(() => {}),
- close: vi.fn(async () => {}),
- };
- return host;
-};
-const metaCatalog = {
- providerId: "meta",
- models: [
- {
- modelId: "muse-discovered",
- displayLabel: "Muse Discovered",
- providerId: "meta",
- isDefault: true,
- },
- {
- modelId: "foreign-model",
- displayLabel: "Another provider",
- providerId: "another",
- isDefault: false,
- },
- ],
-};
-
-const fakeCli = (source = 'process.stdout.write("muse 1.0.3-R2198.1\\n");') =>
- Effect.gen(function* () {
- const fs = yield* FileSystem.FileSystem;
- const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-status-test-" });
- return writeFakeCli({ directory, name: "muse", source });
- });
-
-it.layer(NodeServices.layer)("Muse status", (it) => {
- it.effect("does not start a host when disabled", () =>
- Effect.gen(function* () {
- const createHost = vi.fn(async () => makeHost(metaCatalog));
- const snapshot = yield* checkMuseProviderStatus(settings({}), {}, undefined, createHost);
- expect(snapshot.status).toBe("disabled");
- expect(createHost).not.toHaveBeenCalled();
- }),
- );
-
- it.effect("gives host-local install and login guidance for a missing binary", () =>
- Effect.gen(function* () {
- const snapshot = yield* checkMuseProviderStatus(
- settings({ enabled: true, binaryPath: "/definitely-missing/muse" }),
- );
- expect(snapshot.installed).toBe(false);
- expect(snapshot.status).toBe("error");
- expect(snapshot.message).toContain("muse login");
- expect(snapshot.message).toContain("this T3 server host");
- }),
- );
-
- it.effect("does not expose CLI stderr or start an SDK host after a failed version check", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const binaryPath = yield* fakeCli(
- 'process.stderr.write("secret-test-value");process.exit(2);',
- );
- const createHost = vi.fn(async () => makeHost(metaCatalog));
- const snapshot = yield* checkMuseProviderStatus(
- settings({ enabled: true, binaryPath }),
- undefined,
- undefined,
- createHost,
- );
- expect(snapshot.installed).toBe(true);
- expect(snapshot.status).toBe("error");
- expect(snapshot.message).not.toContain("secret-test-value");
- expect(createHost).not.toHaveBeenCalled();
- }),
- ),
- );
-
- it.effect("discovers the Meta catalog without treating it as authentication proof", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const binaryPath = yield* fakeCli();
- const host = makeHost(metaCatalog);
- const snapshot = yield* checkMuseProviderStatus(
- settings({ enabled: true, binaryPath, customModels: ["muse-custom"] }),
- undefined,
- undefined,
- async () => host,
- );
- expect(snapshot.status).toBe("ready");
- expect(snapshot.message).toBeUndefined();
- expect(snapshot.slashCommands).toEqual([COMPACT_SLASH_COMMAND]);
- expect(snapshot.auth).toEqual({ status: "unknown" });
- expect(snapshot.version).toBe("1.0.3-R2198.1");
- expect(snapshot.models.map((model) => model.slug)).toEqual([
- "muse-discovered",
- "muse-custom",
- ]);
- const descriptor = snapshot.models[0]?.capabilities?.optionDescriptors?.[0];
- expect(
- descriptor?.type === "select" && descriptor.options.map((option) => option.id),
- ).toEqual(["low", "medium", "high", "xhigh", "max"]);
- expect(host.connection.request).toHaveBeenCalledWith("model/list", {});
- expect(host.connection.command).not.toHaveBeenCalled();
- expect(host.close).toHaveBeenCalledOnce();
- }),
- ),
- );
-
- it.effect("closes the host after malformed metadata and reports unavailable", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const binaryPath = yield* fakeCli();
- const host = makeHost({ providerId: "meta", models: [{ modelId: 1 }] });
- const snapshot = yield* checkMuseProviderStatus(
- settings({ enabled: true, binaryPath }),
- undefined,
- undefined,
- async () => host,
- );
- expect(snapshot.status).toBe("error");
- expect(snapshot.auth.status).toBe("unknown");
- expect(snapshot.models).toEqual([]);
- expect(host.close).toHaveBeenCalledOnce();
- }),
- ),
- );
-
- it.effect("keeps an empty discovered catalog empty instead of advertising a fallback model", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const binaryPath = yield* fakeCli();
- const host = makeHost({ providerId: "meta", models: [] });
- const snapshot = yield* checkMuseProviderStatus(
- settings({ enabled: true, binaryPath }),
- undefined,
- undefined,
- async () => host,
- );
- expect(snapshot.status).toBe("warning");
- expect(snapshot.models).toEqual([]);
- expect(host.close).toHaveBeenCalledOnce();
- }),
- ),
- );
-});
diff --git a/apps/server/src/provider/MuseProvider.ts b/apps/server/src/provider/MuseProvider.ts
deleted file mode 100644
index 3d44431d0564bb70d064efb708c00f9d5d74d27c..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/MuseProvider.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import type { MuseSettings, ServerProviderModel } from "@t3tools/contracts";
-import { resolveSpawnCommand } from "@t3tools/shared/shell";
-import * as DateTime from "effect/DateTime";
-import * as Effect from "effect/Effect";
-import * as Option from "effect/Option";
-import * as Result from "effect/Result";
-import { ChildProcess } from "effect/process";
-
-import { createMuseSdkHost, makeMuseEnvironment } from "./museSdk.ts";
-import { parseMuseVersion } from "./museMaintenance.ts";
-import { discoverMuseModels, museModelCapabilities } from "./museModelCatalog.ts";
-import {
- buildServerProvider,
- COMPACT_SLASH_COMMAND,
- DEFAULT_TIMEOUT_MS,
- isCommandMissingCause,
- providerModelsFromSettings,
- spawnAndCollect,
- type ProviderProbeResult,
-} from "@t3tools/provider-core/server/snapshotProbe";
-
-const MUSE_PRESENTATION = {
- displayName: "Muse Code",
- showInteractionModeToggle: false,
- reportsContextWindow: true,
- // Muse has no native "accept edits" or reviewer-backed mode, so T3 offers only
- // the two it maps directly: promptUnmatched and allowAll.
- supportedRuntimeModes: ["approval-required", "full-access"],
-} as const;
-
-const FALLBACK_CAPABILITIES = museModelCapabilities();
-
-export const makePendingMuseProvider = Effect.fn("makePendingMuseProvider")(function* (
- settings: MuseSettings,
-) {
- return buildServerProvider({
- presentation: MUSE_PRESENTATION,
- enabled: settings.enabled,
- checkedAt: DateTime.formatIso(yield* DateTime.now),
- models: providerModelsFromSettings([], settings.customModels, FALLBACK_CAPABILITIES),
- slashCommands: settings.enabled ? [COMPACT_SLASH_COMMAND] : [],
- probe: {
- installed: false,
- version: null,
- status: "warning",
- auth: { status: "unknown" },
- message: settings.enabled
- ? "Checking Muse Code CLI availability..."
- : "Muse Code is disabled in T3 Code settings.",
- },
- });
-});
-
-export const checkMuseProviderStatus = Effect.fn("checkMuseProviderStatus")(function* (
- settings: MuseSettings,
- environment?: NodeJS.ProcessEnv,
- cwd?: string,
- createHost: typeof createMuseSdkHost = createMuseSdkHost,
-) {
- if (!settings.enabled) return yield* makePendingMuseProvider(settings);
- const checkedAt = DateTime.formatIso(yield* DateTime.now);
- // The driver passes a prepared environment; direct callers get the host's without META_API_KEY.
- const museEnvironment = environment ?? makeMuseEnvironment();
- const snapshot = (probe: ProviderProbeResult, models: ReadonlyArray = []) =>
- buildServerProvider({
- presentation: MUSE_PRESENTATION,
- enabled: true,
- checkedAt,
- models: providerModelsFromSettings(models, settings.customModels, FALLBACK_CAPABILITIES),
- slashCommands: [COMPACT_SLASH_COMMAND],
- probe,
- });
- const versionResult = yield* Effect.gen(function* () {
- const spawnCommand = yield* resolveSpawnCommand(settings.binaryPath, ["--version"], {
- env: museEnvironment,
- });
- return yield* spawnAndCollect(
- settings.binaryPath,
- ChildProcess.make(spawnCommand.command, spawnCommand.args, {
- env: museEnvironment,
- shell: spawnCommand.shell,
- ...(cwd ? { cwd } : {}),
- }),
- );
- }).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
-
- if (Result.isFailure(versionResult)) {
- const missing = isCommandMissingCause(versionResult.failure);
- return snapshot({
- installed: !missing,
- version: null,
- status: "error",
- auth: { status: "unknown" },
- message: missing
- ? "Muse Code CLI (`muse`) was not found. Install Muse Code and run `muse login` on this T3 server host."
- : "Failed to execute Muse Code CLI. Check its binary path on this T3 server host.",
- });
- }
- if (Option.isNone(versionResult.success)) {
- return snapshot({
- installed: true,
- version: null,
- status: "error",
- auth: { status: "unknown" },
- message: "Muse Code CLI version check timed out.",
- });
- }
- const versionOutput = versionResult.success.value;
- const version = parseMuseVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`);
- if (versionOutput.code !== 0) {
- return snapshot({
- installed: true,
- version,
- status: "error",
- auth: { status: "unknown" },
- message: "Muse Code CLI is installed but failed to run.",
- });
- }
-
- const catalog = yield* discoverMuseModels(settings, museEnvironment, cwd, createHost).pipe(
- Effect.scoped,
- Effect.timeoutOption(12_000),
- Effect.result,
- );
- if (Result.isFailure(catalog) || Option.isNone(catalog.success)) {
- return snapshot({
- installed: true,
- version,
- status: "error",
- auth: { status: "unknown" },
- message:
- "Muse Code SDK could not read the model catalog. Check your Muse installation and run `muse login` on this T3 server host.",
- });
- }
- const models = catalog.success.value;
- // MSP has no auth signal, and model/list answers from Muse's cache even when logged out.
- // Auth failures surface on the first turn instead.
- return snapshot(
- {
- installed: true,
- version,
- auth: { status: "unknown" },
- ...(models.length > 0
- ? { status: "ready" }
- : {
- status: "warning",
- message:
- "Muse Code returned no models. Run `muse login` on this T3 server host and refresh its status.",
- }),
- },
- models,
- );
-});
diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts
index 08a403b70e048fba30fcaf00fbbe3940522ce725..1690a998c5f4016117d9a6bb154e06e402bcb418 100644
--- a/apps/server/src/provider/builtInDrivers.ts
+++ b/apps/server/src/provider/builtInDrivers.ts
@@ -27,7 +27,7 @@ import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
import { CursorDriver, type CursorDriverEnv } from "./Drivers/CursorDriver.ts";
import { GrokDriver, type GrokDriverEnv } from "./Drivers/GrokDriver.ts";
import { OpenCodeDriver, type OpenCodeDriverEnv } from "./Drivers/OpenCodeDriver.ts";
-import { MuseDriver, type MuseDriverEnv } from "./Drivers/MuseDriver.ts";
+import { MuseDriver, type MuseDriverEnv } from "@t3tools/provider-muse/server";
import { PiDriver, type PiDriverEnv } from "@t3tools/provider-pi/server";
import type { AnyProviderDriver } from "@t3tools/provider-core/server/driver";
diff --git a/apps/server/src/provider/museMaintenance.test.ts b/apps/server/src/provider/museMaintenance.test.ts
deleted file mode 100644
index 59d0868e768ddb528947d2ca3bdb2ccf63025bd4..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museMaintenance.test.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { expect, it } from "@effect/vitest";
-import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import { HttpClient, HttpClientResponse } from "effect/http";
-
-import {
- compareMuseVersions,
- enrichMuseSnapshot,
- museMaintenance,
- latestMuseVersion,
- parseMuseVersion,
-} from "./museMaintenance.ts";
-import {
- ProviderVersionCache,
- makeManualOnlyProviderMaintenanceCapabilities,
-} from "@t3tools/provider-core/server/maintenanceResolver";
-
-const maintenanceCapabilities = {
- ...makeManualOnlyProviderMaintenanceCapabilities({
- provider: ProviderDriverKind.make("muse"),
- packageName: null,
- }),
- compareVersions: compareMuseVersions,
-};
-const snapshot: ServerProvider = {
- instanceId: ProviderInstanceId.make("muse"),
- driver: ProviderDriverKind.make("muse"),
- enabled: true,
- installed: true,
- version: "1.0.3",
- status: "ready",
- auth: { status: "unknown" },
- checkedAt: "2026-09-10T00:00:00.000Z",
- models: [],
- skills: [],
- slashCommands: [],
-};
-
-it.layer(NodeServices.layer)("Muse maintenance", (it) => {
- it.effect(
- "refreshes a cached channel failure when an explicit update requests verification",
- () =>
- Effect.gen(function* () {
- let requests = 0;
- const check = Effect.gen(function* () {
- expect(yield* latestMuseVersion({})).toBeNull();
- expect(yield* latestMuseVersion({})).toBeNull();
- expect(yield* latestMuseVersion({}, { fresh: true })).toBe("1.1.1-R10.1");
- });
- yield* check.pipe(
- Effect.provideService(ProviderVersionCache, new Map()),
- Effect.provideService(
- HttpClient.HttpClient,
- HttpClient.make((request) => {
- requests += 1;
- return Effect.succeed(
- HttpClientResponse.fromWeb(
- request,
- requests === 1
- ? new Response("unavailable", { status: 503 })
- : Response.json({ version: "1.1.1-R10.1" }),
- ),
- );
- }),
- ),
- );
- expect(requests).toBe(2);
- }),
- );
- it.effect(
- "compares numeric native release revisions and preserves the CLI release identity",
- () =>
- Effect.sync(() => {
- expect(parseMuseVersion("Muse Code 1.1.1 (1.1.1-R2514.1)")).toBe("1.1.1-R2514.1");
- expect(compareMuseVersions("1.1.1-R9.1", "1.1.1-R10.1")).toBeLessThan(0);
- expect(compareMuseVersions("1.1.1-R2514.1", "1.1.1-R2514.2")).toBeLessThan(0);
- expect(compareMuseVersions("1.2.0-R1.1", "1.1.1-R2514.2")).toBeGreaterThan(0);
- // A CLI that prints no revision compares by release, never as an older prerelease.
- expect(compareMuseVersions("1.4.3", "1.4.3-R5018.1")).toBe(0);
- expect(compareMuseVersions("1.4.2", "1.4.3-R5018.1")).toBeLessThan(0);
- }),
- );
- it.effect(
- "finds releases from the instance's channel and caches the native version, not the SDK version",
- () =>
- Effect.gen(function* () {
- const requests: string[] = [];
- const httpClient = HttpClient.make((request) => {
- requests.push(request.url);
- return Effect.succeed(
- HttpClientResponse.fromWeb(request, Response.json({ version: "1.1.1-R2514.1" })),
- );
- });
- const check = enrichMuseSnapshot({
- snapshot,
- maintenanceCapabilities,
- enableProviderUpdateChecks: true,
- environment: { MUSE_CHANNEL: "muse-canary" },
- });
- const [first, second] = yield* Effect.all([check, check], { concurrency: 1 }).pipe(
- Effect.provideService(HttpClient.HttpClient, httpClient),
- Effect.provideService(ProviderVersionCache, new Map()),
- );
- expect(first.versionAdvisory).toMatchObject({
- status: "behind_latest",
- currentVersion: "1.0.3",
- latestVersion: "1.1.1-R2514.1",
- canUpdate: false,
- });
- expect(second.versionAdvisory?.latestVersion).toBe("1.1.1-R2514.1");
- expect(requests).toEqual(["https://api.meta.ai/muse-code/channels/muse-canary"]);
- }),
- );
-
- it.effect("reports native release R9 behind R10 and R10 current against R9", () =>
- Effect.gen(function* () {
- for (const [current, latest, status] of [
- ["1.4.2-R9.1", "1.4.2-R10.1", "behind_latest"],
- ["1.4.2-R10.1", "1.4.2-R9.1", "current"],
- ] as const) {
- const result = yield* enrichMuseSnapshot({
- snapshot: { ...snapshot, version: current },
- maintenanceCapabilities,
- enableProviderUpdateChecks: true,
- environment: {},
- }).pipe(
- Effect.provideService(
- HttpClient.HttpClient,
- HttpClient.make((request) =>
- Effect.succeed(
- HttpClientResponse.fromWeb(request, Response.json({ version: latest })),
- ),
- ),
- ),
- Effect.provideService(ProviderVersionCache, new Map()),
- );
- expect(result.versionAdvisory?.status).toBe(status);
- }
- }),
- );
-
- it.effect("skips network checks when disabled, uninstalled or disabled in settings", () =>
- Effect.gen(function* () {
- for (const input of [
- { snapshot, enableProviderUpdateChecks: false },
- { snapshot: { ...snapshot, enabled: false }, enableProviderUpdateChecks: true },
- { snapshot: { ...snapshot, installed: false }, enableProviderUpdateChecks: true },
- ]) {
- const result = yield* enrichMuseSnapshot({
- ...input,
- maintenanceCapabilities,
- environment: {},
- }).pipe(
- Effect.provideService(
- HttpClient.HttpClient,
- HttpClient.make(() => Effect.die("Unexpected release request")),
- ),
- );
- expect(result.versionAdvisory?.status).toBe("unknown");
- }
- }),
- );
-
- it.effect("tolerates invalid release manifests and unavailable channels", () =>
- Effect.gen(function* () {
- for (const response of [
- Response.json({ version: 12 }),
- Response.json({ version: "1.1.1" }),
- new Response("unavailable", { status: 503 }),
- ]) {
- const result = yield* enrichMuseSnapshot({
- snapshot,
- maintenanceCapabilities,
- enableProviderUpdateChecks: true,
- environment: {},
- }).pipe(
- Effect.provideService(
- HttpClient.HttpClient,
- HttpClient.make((request) =>
- Effect.succeed(HttpClientResponse.fromWeb(request, response)),
- ),
- ),
- Effect.provideService(ProviderVersionCache, new Map()),
- );
- expect(result.status).toBe("ready");
- expect(result.versionAdvisory?.status).toBe("unknown");
- }
- }),
- );
-
- it.effect("updates only a recognized launcher at the resolved instance path", () =>
- Effect.scoped(
- Effect.gen(function* () {
- const fs = yield* FileSystem.FileSystem;
- const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-maintenance-" });
- const binaryPath = `${directory}/custom muse`;
- const context = {
- binaryPath,
- resolvedCommandPath: binaryPath,
- realCommandPath: binaryPath,
- platform: "linux" as const,
- env: { MUSE_CHANNEL: "muse-canary" },
- };
- yield* fs.writeFileString(
- binaryPath,
- "#!/bin/bash\n# muse-code/launcher-2\n# MUSE_SYNC_UPDATE\n",
- );
- const result = yield* museMaintenance.resolve(context);
- expect(result.update).toMatchObject({
- executable: binaryPath,
- args: ["--version"],
- lockKey: `muse:${binaryPath}`,
- env: {
- MUSE_CHANNEL: "muse-canary",
- MUSE_NO_AUTO_UPDATE: "0",
- MUSE_SYNC_UPDATE: "1",
- MUSE_UPDATE_INTERVAL_SECONDS: "0",
- },
- });
- expect(result.update?.command).toBe(
- `MUSE_CHANNEL=muse-canary MUSE_NO_AUTO_UPDATE=0 MUSE_SYNC_UPDATE=1 MUSE_UPDATE_INTERVAL_SECONDS=0 '${binaryPath}' --version`,
- );
- expect(result.compareVersions).toBe(compareMuseVersions);
- yield* fs.writeFileString(binaryPath, "standalone binary");
- expect((yield* museMaintenance.resolve(context)).update).toBeNull();
- expect((yield* museMaintenance.resolve(null)).update).toBeNull();
- }),
- ),
- );
-});
diff --git a/apps/server/src/provider/museMaintenance.ts b/apps/server/src/provider/museMaintenance.ts
deleted file mode 100644
index 0770b494a819176657236f0ca502d1d83c8c8c42..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museMaintenance.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
-import { compareSemverVersions } from "@t3tools/shared/semver";
-import * as DateTime from "effect/DateTime";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Schema from "effect/Schema";
-import { HttpClient, HttpClientRequest } from "effect/http";
-
-import {
- enrichProviderSnapshotWithVersionAdvisory,
- makeManualOnlyProviderMaintenanceCapabilities,
- makeProviderMaintenanceCapabilities,
- ProviderVersionCache,
- type ProviderMaintenanceCapabilities,
- type ProviderMaintenanceCapabilitiesResolver,
-} from "@t3tools/provider-core/server/maintenanceResolver";
-import { parseGenericCliVersion } from "@t3tools/provider-core/server/snapshotProbe";
-
-const DRIVER = ProviderDriverKind.make("muse");
-/** Mirrors the official launcher, which reads MUSE_CHANNEL and accepts only these two. */
-const MUSE_CHANNELS = new Set(["muse-stable", "muse-canary"]);
-const ChannelManifest = Schema.Struct({
- version: Schema.String.check(Schema.isPattern(/^\d+\.\d+\.\d+-R\d+(?:\.\d+)?$/)),
-});
-
-/** Muse prints both a display version and a release revision; prefer the latter. */
-export function parseMuseVersion(output: string): string | null {
- return output.match(/\b\d+\.\d+\.\d+-R\d+(?:\.\d+)?\b/)?.[0] ?? parseGenericCliVersion(output);
-}
-
-function comparableMuseVersion(version: string): string {
- // Make the release sequence numeric for semver comparison (R9 precedes R10).
- return version.replace(/-R(\d+)(?:\.(\d+))?$/, "-release.$1.$2").replace(/\.$/, ".0");
-}
-
-const MUSE_REVISION = /-R\d+(?:\.\d+)?$/;
-
-/** A version printed without a revision is compared by its release alone. */
-export const compareMuseVersions = (current: string, latest: string) =>
- MUSE_REVISION.test(current) === MUSE_REVISION.test(latest)
- ? compareSemverVersions(comparableMuseVersion(current), comparableMuseVersion(latest))
- : compareSemverVersions(current.replace(MUSE_REVISION, ""), latest.replace(MUSE_REVISION, ""));
-
-/** Only the official launcher owns an update channel; a standalone binary cannot update itself. */
-export const museMaintenance: ProviderMaintenanceCapabilitiesResolver = {
- resolve: (context) =>
- Effect.gen(function* () {
- const manual = {
- ...makeManualOnlyProviderMaintenanceCapabilities({ provider: DRIVER, packageName: null }),
- compareVersions: compareMuseVersions,
- };
- if (!context || context.platform === "win32") return manual;
- const fs = yield* FileSystem.FileSystem;
- const isLauncher = yield* Effect.gen(function* () {
- const stat = yield* fs.stat(context.realCommandPath);
- if (Number(stat.size) > 128 * 1_024) return false;
- const source = yield* fs.readFileString(context.realCommandPath);
- return source.includes("muse-code/launcher-") && source.includes("MUSE_SYNC_UPDATE");
- }).pipe(Effect.orElseSucceed(() => false));
- if (!isLauncher) return manual;
- // The launcher has no `update` subcommand. Its documented launch environment
- // forces a synchronous refresh of this executable's own installation.
- const updateEnv = {
- ...(context.env.MUSE_CHANNEL ? { MUSE_CHANNEL: context.env.MUSE_CHANNEL } : {}),
- MUSE_NO_AUTO_UPDATE: "0",
- MUSE_SYNC_UPDATE: "1",
- MUSE_UPDATE_INTERVAL_SECONDS: "0",
- };
- return {
- ...makeProviderMaintenanceCapabilities({
- provider: DRIVER,
- packageName: null,
- updateExecutable: context.resolvedCommandPath,
- updateArgs: ["--version"],
- updateCommand: [
- ...Object.entries(updateEnv).map(([key, value]) => `${key}=${shellQuote(value)}`),
- shellQuote(context.resolvedCommandPath),
- "--version",
- ].join(" "),
- updateLockKey: `muse:${context.realCommandPath}`,
- platform: context.platform,
- env: { ...context.env, ...updateEnv },
- }),
- compareVersions: compareMuseVersions,
- };
- }),
-};
-
-function shellQuote(word: string) {
- return /^[\w./:@=-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
-}
-
-export const latestMuseVersion = Effect.fn("latestMuseVersion")(function* (
- environment: NodeJS.ProcessEnv,
- options?: { readonly fresh?: boolean },
-) {
- const channel = environment.MUSE_CHANNEL || "muse-stable";
- if (!MUSE_CHANNELS.has(channel)) return null;
- const channelUrl = `https://api.meta.ai/muse-code/channels/${channel}`;
- const cache = yield* ProviderVersionCache;
- const key = `muse:${channelUrl}`;
- const now = DateTime.toEpochMillis(yield* DateTime.now);
- const cached = cache.get(key);
- if (!options?.fresh && cached && cached.expiresAt > now) return cached.version;
- const client = yield* HttpClient.HttpClient;
- const version = yield* client.execute(HttpClientRequest.get(channelUrl)).pipe(
- Effect.flatMap((response) =>
- response.status >= 200 && response.status < 300
- ? response.json.pipe(
- Effect.flatMap(Schema.decodeUnknownEffect(ChannelManifest)),
- Effect.map((manifest) => parseMuseVersion(manifest.version)),
- )
- : Effect.succeed(null),
- ),
- Effect.timeout(4_000),
- Effect.orElseSucceed(() => null),
- );
- cache.set(key, { version, expiresAt: now + (version === null ? 60_000 : 60 * 60 * 1_000) });
- return version;
-});
-
-/** Feed Muse's release channel into the same advisory and notification flow as other drivers. */
-export const enrichMuseSnapshot = Effect.fn("enrichMuseSnapshot")(function* (input: {
- readonly snapshot: ServerProvider;
- readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
- readonly enableProviderUpdateChecks: boolean;
- readonly environment: NodeJS.ProcessEnv;
-}) {
- const { snapshot } = input;
- const latestVersion =
- input.enableProviderUpdateChecks && snapshot.enabled && snapshot.installed && snapshot.version
- ? yield* latestMuseVersion(input.environment)
- : null;
- return yield* enrichProviderSnapshotWithVersionAdvisory(
- snapshot,
- { ...input.maintenanceCapabilities, latestVersion },
- { enableProviderUpdateChecks: input.enableProviderUpdateChecks },
- );
-});
diff --git a/apps/server/src/provider/museModelCatalog.test.ts b/apps/server/src/provider/museModelCatalog.test.ts
deleted file mode 100644
index 2679be85e85350de213b715fa3a50e495fe540ce..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museModelCatalog.test.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { describe, expect, it } from "@effect/vitest";
-import { MuseSettings } from "@t3tools/contracts";
-import * as Effect from "effect/Effect";
-import * as Schema from "effect/Schema";
-
-import type { MuseSdkHost } from "./museSdk.ts";
-import {
- discoverMuseModels,
- museModelCapabilities,
- resolveMuseReasoningEffort,
-} from "./museModelCatalog.ts";
-
-const settings = Schema.decodeSync(MuseSettings)({});
-const host = (catalog: Record): MuseSdkHost => ({
- connection: {
- request: async () => catalog,
- command: async () => ({}),
- mintCommandId: () => "test",
- onNotification: () => {},
- onServerRequest: () => {},
- onProtocolError: () => {},
- closed: new Promise(() => {}),
- },
- initializeResult: { grantedCapabilities: [] },
- exited: new Promise(() => {}),
- close: async () => {},
-});
-
-const row = (modelId: string, extra: Record = {}) => ({
- modelId,
- displayLabel: modelId,
- providerId: "meta",
- profileId: "tbh",
- isDefault: false,
- contextLimit: 1_000_000,
- ...extra,
-});
-
-describe("Muse model catalog", () => {
- it.effect("uses the labels, efforts and defaults that model/list sends", () =>
- Effect.gen(function* () {
- const models = yield* discoverMuseModels(settings, {}, undefined, async () =>
- host({
- providerId: "meta",
- profileId: "tbh",
- source: "providerCatalog",
- models: [
- row("restful-walrus", {
- displayLabel: " Restful Walrus ",
- isDefault: true,
- variants: ["minimal", "low", "high", "future-tier"],
- reasoningEffortVariants: [{ tier: "low", description: "Quick" }],
- defaultReasoningEffort: "high",
- }),
- row("older-host"),
- row("unknown-efforts", { variants: "unknown" }),
- row("foreign", { providerId: "another" }),
- ],
- }),
- ).pipe(Effect.scoped);
- expect(models.map(({ slug, name, isDefault }) => [slug, name, isDefault])).toEqual([
- ["restful-walrus", "Restful Walrus", true],
- ["older-host", "older-host", false],
- ["unknown-efforts", "unknown-efforts", false],
- ]);
- expect(models[0]?.capabilities?.optionDescriptors).toEqual([
- {
- id: "reasoningEffort",
- label: "Reasoning",
- type: "select",
- currentValue: "high",
- options: [
- { id: "minimal", label: "Minimal" },
- { id: "low", label: "Low", description: "Quick" },
- { id: "high", label: "High", isDefault: true },
- ],
- },
- ]);
- // Older hosts and "unknown" fall back to Muse's documented tiers.
- expect(models[1]?.capabilities).toEqual(museModelCapabilities());
- expect(models[2]?.capabilities).toEqual(museModelCapabilities());
- }),
- );
-
- it("normalizes saved and implicit efforts against the selected model before dispatch", () => {
- const fallback = museModelCapabilities();
- expect(resolveMuseReasoningEffort(fallback, undefined)).toBe("high");
- expect(resolveMuseReasoningEffort(fallback, "max")).toBe("max");
- expect(resolveMuseReasoningEffort(fallback, "ultra")).toBe("high");
- const restricted = museModelCapabilities({ variants: ["xhigh"] });
- expect(resolveMuseReasoningEffort(restricted, undefined)).toBe("xhigh");
- expect(resolveMuseReasoningEffort(restricted, "low")).toBe("xhigh");
- expect(
- resolveMuseReasoningEffort(museModelCapabilities({ variants: [] }), "max"),
- ).toBeUndefined();
- expect(resolveMuseReasoningEffort(undefined, "max")).toBe("max");
- });
-});
diff --git a/apps/server/src/provider/museModelCatalog.ts b/apps/server/src/provider/museModelCatalog.ts
deleted file mode 100644
index 7a75cf26f6d5851e8181b8e358c79b885efb4b61..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museModelCatalog.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import type { SendUserTurnOptions } from "@muse-code/sdk";
-import type { ModelCapabilities, MuseSettings, ServerProviderModel } from "@t3tools/contracts";
-import { createModelCapabilities, getProviderOptionDescriptors } from "@t3tools/shared/model";
-import * as Effect from "effect/Effect";
-import * as Schema from "effect/Schema";
-import { createMuseSdkHost, createMuseSdkHostEffect, type MuseSdkHost } from "./museSdk.ts";
-
-type MuseReasoningEffort = NonNullable["reasoningEffort"]>;
-
-const REASONING_EFFORT_LABELS = {
- none: "None",
- minimal: "Minimal",
- low: "Low",
- medium: "Medium",
- high: "High",
- xhigh: "Extra High",
- max: "Max",
- ultra: "Ultra",
-} satisfies Record;
-
-const isMuseReasoningEffort = (tier: string): tier is MuseReasoningEffort =>
- Object.hasOwn(REASONING_EFFORT_LABELS, tier);
-
-/** Used when Muse does not report a model's efforts (custom slugs, older hosts, "unknown"). */
-const FALLBACK_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
-/** Muse's own CLI default. */
-const FALLBACK_DEFAULT_EFFORT = "high";
-
-const ModelCatalogEntry = Schema.Struct({
- modelId: Schema.NonEmptyString,
- displayLabel: Schema.String,
- providerId: Schema.String,
- isDefault: Schema.Boolean,
- variants: Schema.optional(Schema.Union([Schema.Array(Schema.String), Schema.Literal("unknown")])),
- defaultReasoningEffort: Schema.optional(Schema.String),
- reasoningEffortVariants: Schema.optional(
- Schema.Array(
- Schema.Struct({ tier: Schema.String, description: Schema.optional(Schema.String) }),
- ),
- ),
-});
-const decodeModelList = Schema.decodeUnknownEffect(
- Schema.Struct({ providerId: Schema.String, models: Schema.Array(ModelCatalogEntry) }),
-);
-
-/** Builds the reasoning picker from a `model/list` row, or the fallback when called without one. */
-export function museModelCapabilities(
- model?: Pick<
- typeof ModelCatalogEntry.Type,
- "variants" | "defaultReasoningEffort" | "reasoningEffortVariants"
- >,
-): ModelCapabilities {
- const reported = model?.variants !== undefined && model.variants !== "unknown";
- const efforts = (reported ? model.variants : FALLBACK_EFFORTS).filter(isMuseReasoningEffort);
- const preferredDefault = reported ? model?.defaultReasoningEffort : FALLBACK_DEFAULT_EFFORT;
- const defaultValue = efforts.find((tier) => tier === preferredDefault) ?? efforts[0];
- if (!defaultValue) return createModelCapabilities({ optionDescriptors: [] });
- return createModelCapabilities({
- optionDescriptors: [
- {
- id: "reasoningEffort",
- label: "Reasoning",
- type: "select",
- currentValue: defaultValue,
- options: efforts.map((id) => {
- const description = model?.reasoningEffortVariants?.find(
- (variant) => variant.tier === id,
- )?.description;
- return {
- id,
- label: REASONING_EFFORT_LABELS[id],
- ...(id === defaultValue ? { isDefault: true } : {}),
- ...(description ? { description } : {}),
- };
- }),
- },
- ],
- });
-}
-
-/** Apply the advertised model choices to saved, remembered and implicit efforts at dispatch. */
-export function resolveMuseReasoningEffort(
- capabilities: ModelCapabilities | null | undefined,
- effort: string | undefined,
-): string | undefined {
- if (!capabilities) return effort;
- const descriptor = getProviderOptionDescriptors({
- caps: capabilities,
- selections: effort ? [{ id: "reasoningEffort", value: effort }] : undefined,
- }).find((descriptor) => descriptor.id === "reasoningEffort");
- if (descriptor?.type !== "select") return undefined;
- return descriptor.options.find((option) => option.id === descriptor.currentValue)?.id;
-}
-
-class MuseCatalogError extends Schema.TaggedError()("MuseCatalogError", {
- detail: Schema.String,
-}) {}
-
-export const discoverMuseModels = Effect.fn("discoverMuseModels")(function* (
- settings: MuseSettings,
- environment: NodeJS.ProcessEnv | undefined,
- cwd?: string,
- createHost: typeof createMuseSdkHost = createMuseSdkHost,
-) {
- const host = yield* Effect.acquireRelease(
- createMuseSdkHostEffect(
- {
- binaryPath: settings.binaryPath,
- ...(environment ? { environment } : {}),
- ...(cwd ? { cwd } : {}),
- readOnly: true,
- startupTimeoutMs: 8_000,
- },
- createHost,
- ),
- (host: MuseSdkHost) => Effect.promise(() => host.close()),
- { interruptible: true },
- );
- const result = yield* Effect.tryPromise(() => host.connection.request("model/list", {}));
- const catalog = yield* decodeModelList(result);
- if (catalog.providerId !== "meta") {
- return yield* new MuseCatalogError({ detail: "Muse returned a catalog for another provider." });
- }
- const seen = new Set();
- return catalog.models.flatMap((model): ServerProviderModel[] => {
- if (model.providerId !== "meta" || seen.has(model.modelId)) return [];
- seen.add(model.modelId);
- return [
- {
- slug: model.modelId,
- name: model.displayLabel.trim() || model.modelId,
- isCustom: false,
- isDefault: model.isDefault,
- capabilities: museModelCapabilities(model),
- },
- ];
- });
-});
diff --git a/apps/server/src/provider/museProtocol.test.ts b/apps/server/src/provider/museProtocol.test.ts
deleted file mode 100644
index 479da19a4fdb99a01a31c9f16938ae708eb5531e..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museProtocol.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { describe, expect, it } from "vite-plus/test";
-
-import { museApprovalChoices } from "./museProtocol.ts";
-
-// Muse 1.4.3 shell approvals offer only `abort`; the muse_permission replay covers that case.
-describe("Muse approval choices", () => {
- it("keeps denied and abort separate when Muse offers both", () => {
- const choices = museApprovalChoices({
- availableChoices: [
- { choiceId: "deny", label: "Deny", decision: "denied", scope: "once" },
- { choiceId: "abort", label: "Stop", decision: "abort", scope: "once" },
- ],
- });
- expect(choices.get("decline")?.choiceId).toBe("deny");
- expect(choices.get("cancel")?.choiceId).toBe("abort");
- });
-});
diff --git a/apps/server/src/provider/museProtocol.ts b/apps/server/src/provider/museProtocol.ts
deleted file mode 100644
index 91befa21e0485d2e3ed2d5125b9d03c0ae7498db..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museProtocol.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import type { ProviderApprovalDecision } from "@t3tools/contracts";
-import * as Schema from "effect/Schema";
-
-const NonEmptyString = Schema.String.check(Schema.isMinLength(1));
-
-const MuseUsage = Schema.Struct({
- inputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- outputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- cachedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- reasoningTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- cacheReadTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
- cacheWriteTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
-});
-
-export const MuseItem = Schema.Struct({
- /** `workflow`: the launched entry's name. */
- entryId: Schema.optional(Schema.String),
- /** `workflow`: each child agent's latest attempt, re-sent whole on every change. */
- children: Schema.optional(
- Schema.Array(
- Schema.Struct({
- childId: NonEmptyString,
- attempt: Schema.Int,
- status: Schema.String,
- label: Schema.optional(Schema.String),
- terminal: Schema.optional(Schema.String),
- durationMs: Schema.optional(Schema.Finite),
- failureReason: Schema.optional(Schema.String),
- }),
- ),
- ),
- itemId: NonEmptyString,
- kind: NonEmptyString,
- revision: Schema.Int.check(Schema.isGreaterThan(0)),
- status: NonEmptyString,
- turnId: Schema.optional(Schema.NullOr(Schema.String)),
- text: Schema.optional(Schema.String),
- summary: Schema.optional(Schema.Array(Schema.String)),
- tool: Schema.optional(Schema.String),
- args: Schema.optional(Schema.String),
- visibleOutput: Schema.optional(Schema.String),
- truncated: Schema.optional(Schema.Boolean),
- failureReason: Schema.optional(Schema.String),
- fallbackText: Schema.optional(Schema.String),
- commandText: Schema.optional(Schema.String),
- exitCode: Schema.optional(Schema.Int),
- durationMs: Schema.optional(Schema.Finite),
- subagentId: Schema.optional(Schema.String),
- childSessionId: Schema.optional(Schema.String),
- objective: Schema.optional(Schema.String),
- role: Schema.optional(Schema.String),
- controlStatus: Schema.optional(Schema.String),
- result: Schema.optional(
- Schema.Struct({ summary: Schema.String, text: Schema.optional(Schema.String) }),
- ),
- usage: Schema.optional(MuseUsage),
- outcome: Schema.optional(Schema.String),
- trigger: Schema.optional(Schema.String),
- reason: Schema.optional(Schema.String),
-});
-export type MuseItem = typeof MuseItem.Type;
-
-export const MuseSessionResult = Schema.Struct({
- session: Schema.Struct({
- sessionId: NonEmptyString,
- modelId: Schema.optional(Schema.NullOr(Schema.String)),
- activeTurnId: Schema.optional(Schema.NullOr(Schema.String)),
- }),
- history: Schema.optional(
- Schema.Struct({
- items: Schema.NullOr(Schema.Array(MuseItem)),
- snapshot: Schema.optional(
- Schema.NullOr(
- Schema.Struct({
- schemaVersion: Schema.optional(Schema.Int),
- state: Schema.Struct({ items: Schema.Array(MuseItem) }),
- }),
- ),
- ),
- }),
- ),
-});
-export const MuseTurnStartResult = Schema.Struct({ turnId: NonEmptyString });
-
-export const MuseApproval = Schema.Struct({
- approvalId: NonEmptyString,
- protectedWrite: Schema.optional(Schema.Boolean),
- judgeEscalated: Schema.optional(Schema.Boolean),
- sessionId: NonEmptyString,
- turnId: Schema.optional(Schema.String),
- itemId: Schema.optional(Schema.String),
- toolName: Schema.optional(Schema.String),
- rawArgs: Schema.optional(Schema.String),
- /** Set when a workflow child asks; `turnId` is then the child's run, not a session turn. */
- subagentOrigin: Schema.optional(Schema.Struct({ subagentId: NonEmptyString })),
- currentRequirementId: Schema.Struct({ approvalId: NonEmptyString, sourceIndex: Schema.Int }),
- availableChoices: Schema.Array(
- Schema.Struct({
- choiceId: NonEmptyString,
- label: Schema.String,
- decision: NonEmptyString,
- scope: Schema.String,
- }),
- ),
- subject: Schema.Struct({
- kind: Schema.String,
- access: Schema.optional(Schema.String),
- command: Schema.optional(Schema.String),
- path: Schema.optional(Schema.String),
- host: Schema.optional(Schema.String),
- target: Schema.optional(Schema.String),
- }),
-});
-export type MuseApproval = typeof MuseApproval.Type;
-
-export const MuseUserInput = Schema.Struct({
- userInputId: NonEmptyString,
- sessionId: NonEmptyString,
- turnId: NonEmptyString,
- questions: Schema.Array(
- Schema.Struct({
- id: NonEmptyString,
- header: Schema.String,
- question: Schema.String,
- options: Schema.Array(
- Schema.Struct({ label: Schema.String, description: Schema.optional(Schema.String) }),
- ),
- selection: Schema.Struct({
- mode: Schema.Literals(["single", "multiple"]),
- minSelections: Schema.optional(Schema.Int),
- maxSelections: Schema.optional(Schema.Int),
- }),
- }),
- ),
-});
-export type MuseUserInput = typeof MuseUserInput.Type;
-
-export const MuseTokenUsageEvent = Schema.Struct({
- turnId: NonEmptyString,
- promptTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- usage: MuseUsage,
-});
-export const MuseContextUsage = Schema.Struct({
- usedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
- windowTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
-});
-export const MuseTodoList = Schema.Struct({
- items: Schema.Array(Schema.Struct({ text: Schema.String, status: Schema.String })),
-});
-export const MuseTurnCompleted = Schema.Struct({
- turnId: NonEmptyString,
- terminal: NonEmptyString,
- reason: Schema.optional(Schema.String),
- error: Schema.optional(Schema.Struct({ message: Schema.String })),
- usage: Schema.optional(MuseUsage),
-});
-export const MuseTurnRetryScheduled = Schema.Struct({
- turnId: NonEmptyString,
- nextAttempt: Schema.Int.check(Schema.isGreaterThan(0)),
- maxAttempts: Schema.Int.check(Schema.isGreaterThan(0)),
- reason: NonEmptyString,
- retryDelayMs: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
-});
-export const MuseCompactResult = Schema.Struct({
- status: Schema.String,
- reason: Schema.optional(Schema.String),
-});
-
-export const MuseItemEvent = Schema.Struct({ item: MuseItem });
-export const MuseDelta = Schema.Struct({
- itemId: NonEmptyString,
- delta: Schema.String,
- field: Schema.optional(Schema.String),
-});
-
-/** The T3 decision for one native Muse choice, or none when T3 has no equivalent. */
-export function museApprovalDecision(
- choice: Pick,
-): ProviderApprovalDecision | undefined {
- switch (choice.decision) {
- case "approved":
- return "accept";
- case "approvedForSession":
- return "acceptForSession";
- case "approvedPolicyAmendment":
- return choice.scope === "session"
- ? "acceptForSession"
- : choice.scope === "localPersistent"
- ? "acceptAlways"
- : undefined;
- case "denied":
- case "deniedPolicyAmendment":
- return "decline";
- case "abort":
- return "cancel";
- default:
- return undefined;
- }
-}
-
-/**
- * Maps each canonical decision to one native choice. Muse shell approvals offer
- * no `denied` choice, only `abort` ("Reject"), so `decline` falls back to it.
- */
-export function museApprovalChoices(approval: Pick) {
- const choices = new Map();
- const rank = (choice: MuseApproval["availableChoices"][number]) =>
- (choice.scope === "once" ? 0 : choice.scope === "session" ? 2 : 4) +
- (choice.decision.endsWith("PolicyAmendment") ? 1 : 0);
- for (const choice of approval.availableChoices) {
- const decision = museApprovalDecision(choice);
- if (!decision) continue;
- const previous = choices.get(decision);
- if (!previous || rank(choice) < rank(previous)) choices.set(decision, choice);
- }
- const abort = choices.get("cancel");
- if (!choices.has("decline") && abort) choices.set("decline", abort);
- return choices;
-}
-
-/** The approval buttons to show: one per native choice, preferring `decline` over `cancel`. */
-export function museApprovalOptions(approval: Pick) {
- const choices = museApprovalChoices(approval);
- const declineChoiceId = choices.get("decline")?.choiceId;
- return [...choices]
- .filter(([decision, choice]) => decision !== "cancel" || choice.choiceId !== declineChoiceId)
- .map(([decision, choice]) => ({ decision, label: choice.label }));
-}
diff --git a/apps/server/src/provider/museSdk.test.ts b/apps/server/src/provider/museSdk.test.ts
deleted file mode 100644
index ff8de0c1abf946839074321a45a5a223d60c95c6..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museSdk.test.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-import { spawnMspConnection, type MspHandshake, type SpawnedMspConnection } from "@muse-code/sdk";
-import { it } from "@effect/vitest";
-import * as Effect from "effect/Effect";
-import * as Fiber from "effect/Fiber";
-import { afterEach, describe, expect, vi } from "vite-plus/test";
-import {
- createMuseSdkHost,
- createMuseSdkHostEffect,
- makeMuseEnvironment,
- museApprovalMode,
- type MuseSdkHost,
-} from "./museSdk.ts";
-
-vi.mock("@muse-code/sdk", () => ({ spawnMspConnection: vi.fn() }));
-
-function pending() {
- let resolve = (_value: A) => {};
- let reject = (_error: unknown) => {};
- const promise = new Promise((yes, no) => {
- resolve = yes;
- reject = no;
- });
- return { promise, resolve, reject };
-}
-
-function mockSpawn() {
- const startup = pending();
- const shutdown = pending<{ code: number; signal: null }>();
- const closing = pending();
- const initializing = pending();
- const initialize = vi.fn(() => {
- initializing.resolve();
- return startup.promise;
- });
- const close = vi.fn(() => {
- closing.resolve();
- return shutdown.promise;
- });
- const handshake = { initialize, close } as unknown as MspHandshake;
- vi.mocked(spawnMspConnection).mockReturnValue(handshake);
- const ready = {
- connection: {},
- initializeResult: { grantedCapabilities: [], schema: { version: 1 } },
- exited: shutdown.promise,
- fingerprintWarning: { warning: "additive optional" },
- } as unknown as SpawnedMspConnection;
- return { startup, shutdown, closing, initializing, initialize, close, ready };
-}
-
-afterEach(() => {
- vi.useRealTimers();
- vi.clearAllMocks();
-});
-
-describe("Muse SDK host", () => {
- it.each(["META_API_KEY", "meta_api_key", "Meta_Api_Key"])(
- "excludes %s without reading its value or mutating the supplied environment",
- (key) => {
- const source: NodeJS.ProcessEnv = { PATH: "/bin", MUSE_TEST: "yes" };
- const read = vi.fn(() => {
- throw new Error("The API override must not be read.");
- });
- Object.defineProperty(source, key, { enumerable: true, get: read });
- expect(makeMuseEnvironment(source)).toEqual({
- PATH: "/bin",
- MUSE_TEST: "yes",
- MUSE_NO_AUTO_UPDATE: "1",
- });
- expect(read).not.toHaveBeenCalled();
- expect(Object.keys(source)).toContain(key);
- },
- );
-
- it("selects full access explicitly and keeps other runtime modes sandboxed", async () => {
- for (const mode of ["approval-required", "auto-accept-edits", "auto", "full-access"] as const) {
- const fake = mockSpawn();
- const running = createMuseSdkHost({
- binaryPath: "/custom/muse",
- cwd: "/workspace",
- runtimeMode: mode,
- environment: { PATH: "/bin" },
- });
- expect(vi.mocked(spawnMspConnection).mock.lastCall?.[0]).toMatchObject({
- command: "/custom/muse",
- cwd: "/workspace",
- args:
- mode === "full-access"
- ? ["serve", "--trust-workspace", "--disable-sandbox"]
- : ["serve", "--trust-workspace"],
- });
- expect(museApprovalMode(mode)).toBe(mode === "full-access" ? "allowAll" : "promptUnmatched");
- fake.startup.resolve(fake.ready);
- const host = await running;
- fake.shutdown.resolve({ code: 0, signal: null });
- await host.close();
- await host.close();
- expect(fake.close).toHaveBeenCalledOnce();
- }
- });
-
- it("bounds startup and waits for native shutdown after timeout", async () => {
- vi.useFakeTimers();
- const fake = mockSpawn();
- const startup = createMuseSdkHost({ binaryPath: "muse", startupTimeoutMs: 30 });
- let settled = false;
- const outcome = startup.then(
- () => {
- settled = true;
- },
- (error: unknown) => {
- settled = true;
- return error;
- },
- );
- await vi.advanceTimersByTimeAsync(31);
- expect(fake.close).toHaveBeenCalledOnce();
- expect(settled).toBe(false);
- fake.shutdown.resolve({ code: 0, signal: null });
- expect(await outcome).toMatchObject({ message: "Muse SDK initialization timed out." });
- });
-
- it("closes the pre-handshake host on abort and rejects only after shutdown", async () => {
- const fake = mockSpawn();
- const abort = new AbortController();
- const started = createMuseSdkHost({ binaryPath: "muse", signal: abort.signal });
- const reason = new Error("Cancelled by caller");
- const outcome = started.catch((error: unknown) => error);
- abort.abort(reason);
- expect(fake.close).toHaveBeenCalledOnce();
- fake.shutdown.resolve({ code: 0, signal: null });
- expect(await outcome).toBe(reason);
- });
-
- it("closes rejected initialization and keeps successful schema advisories usable", async () => {
- const failed = mockSpawn();
- const started = createMuseSdkHost({ binaryPath: "muse" });
- const outcome = started.catch((error: unknown) => error);
- failed.startup.reject(new Error("Handshake failed"));
- failed.shutdown.resolve({ code: 0, signal: null });
- expect(await outcome).toMatchObject({ message: "Handshake failed" });
- expect(failed.close).toHaveBeenCalledOnce();
- const success = mockSpawn();
- const ready = createMuseSdkHost({ binaryPath: "muse", readOnly: true });
- success.startup.resolve(success.ready);
- const host = await ready;
- expect(host.initializeResult.grantedCapabilities).toEqual([]);
- expect(vi.mocked(spawnMspConnection).mock.lastCall?.[0].args).toEqual([
- "serve",
- "--disable-shell",
- "--disable-write",
- "--no-session-log",
- ]);
- success.shutdown.resolve({ code: 0, signal: null });
- await host.close();
- });
-
- it.each([undefined, 2])(
- "rejects unsupported envelope version %s and awaits shutdown",
- async (version) => {
- const fake = mockSpawn();
- const started = createMuseSdkHost({ binaryPath: "muse" });
- let settled = false;
- const outcome = started.catch((error: unknown) => {
- settled = true;
- return error;
- });
- fake.startup.resolve({
- ...fake.ready,
- initializeResult: {
- ...fake.ready.initializeResult,
- schema: { fingerprint: "test", version },
- },
- } as unknown as SpawnedMspConnection);
- await fake.closing.promise;
- expect(fake.close).toHaveBeenCalledOnce();
- expect(settled).toBe(false);
- fake.shutdown.resolve({ code: 0, signal: null });
- expect(await outcome).toMatchObject({
- message: "Muse SDK returned an unsupported protocol envelope version.",
- });
- },
- );
-
- it("does not spawn when already aborted and closes a ready host on later abort", async () => {
- const aborted = new AbortController();
- aborted.abort();
- await expect(
- createMuseSdkHost({ binaryPath: "muse", signal: aborted.signal }),
- ).rejects.toBeDefined();
- expect(spawnMspConnection).not.toHaveBeenCalled();
- const fake = mockSpawn();
- const abort = new AbortController();
- const running = createMuseSdkHost({ binaryPath: "muse", signal: abort.signal });
- fake.startup.resolve(fake.ready);
- const host = await running;
- abort.abort();
- fake.shutdown.resolve({ code: 0, signal: null });
- await host.close();
- expect(fake.close).toHaveBeenCalledOnce();
- });
-
- it.effect(
- "keeps Effect startup resources alive until interrupted initialization shuts down",
- () =>
- Effect.gen(function* () {
- const fake = mockSpawn();
- let released = false;
- const fiber = yield* Effect.forkChild(
- createMuseSdkHostEffect({ binaryPath: "muse" }).pipe(
- Effect.ensuring(
- Effect.sync(() => {
- released = true;
- }),
- ),
- ),
- );
- yield* Effect.promise(() => fake.initializing.promise);
- const interrupted = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild);
- yield* Effect.promise(() => fake.closing.promise);
- expect(released).toBe(false);
- fake.shutdown.resolve({ code: 0, signal: null });
- yield* Fiber.join(interrupted);
- expect(released).toBe(true);
- expect(fake.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("closes a fulfilled host when interruption wins before acquisition is delivered", () =>
- Effect.gen(function* () {
- const started = pending();
- const startup = pending();
- const closing = pending();
- const shutdown = pending();
- const host = {
- close: vi.fn(() => {
- closing.resolve();
- return shutdown.promise;
- }),
- } as unknown as MuseSdkHost;
- let released = false;
- const fiber = yield* Effect.forkChild(
- createMuseSdkHostEffect({ binaryPath: "muse" }, () => {
- started.resolve();
- return startup.promise;
- }).pipe(
- Effect.ensuring(
- Effect.sync(() => {
- released = true;
- }),
- ),
- ),
- );
- yield* Effect.promise(() => started.promise);
- startup.resolve(host);
- const interrupted = yield* Fiber.interrupt(fiber).pipe(
- Effect.forkChild({ startImmediately: true }),
- );
- yield* Effect.promise(() => closing.promise);
- expect(released).toBe(false);
- shutdown.resolve();
- yield* Fiber.join(interrupted);
- expect(released).toBe(true);
- expect(host.close).toHaveBeenCalledOnce();
- }),
- );
-});
diff --git a/apps/server/src/provider/museSdk.ts b/apps/server/src/provider/museSdk.ts
deleted file mode 100644
index dcc778d4f77973d25832451b7bcd47f6390342cb..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/museSdk.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-// @effect-diagnostics globalTimers:off
-// The SDK owns process shutdown; native deadlines cover initialize before an Effect resource exists.
-import {
- spawnMspConnection,
- type Connection,
- type ProcessExit,
- type SpawnedMspConnection,
-} from "@muse-code/sdk";
-import type { RuntimeMode } from "@t3tools/contracts";
-import * as Effect from "effect/Effect";
-
-export interface MuseSdkHost {
- readonly connection: Pick<
- Connection,
- | "command"
- | "request"
- | "mintCommandId"
- | "onNotification"
- | "onServerRequest"
- | "onProtocolError"
- | "closed"
- >;
- readonly initializeResult: Pick;
- readonly exited: Promise;
- /** The last lines Muse wrote to stderr, for explaining an unexpected exit. */
- readonly stderrTail?: () => ReadonlyArray;
- readonly close: () => Promise;
-}
-
-export interface MuseSdkHostOptions {
- readonly binaryPath: string;
- readonly cwd?: string;
- readonly environment?: NodeJS.ProcessEnv;
- readonly runtimeMode?: RuntimeMode;
- /** Disables shell and filesystem writes and workspace trust. */
- readonly readOnly?: boolean;
- /** Read-only generation needs durable logging for turn/item notifications (verified through 1.1.1). */
- readonly sessionLogging?: boolean;
- readonly signal?: AbortSignal;
- readonly startupTimeoutMs?: number;
-}
-
-/** Preserve the host's CLI login while excluding the API-key override before copying values. */
-export function makeMuseEnvironment(
- environment: NodeJS.ProcessEnv = process.env,
-): NodeJS.ProcessEnv {
- return {
- ...Object.fromEntries(
- Object.keys(environment)
- .filter((key) => key.toUpperCase() !== "META_API_KEY")
- .map((key) => [key, environment[key]]),
- ),
- MUSE_NO_AUTO_UPDATE: "1",
- };
-}
-
-export function museApprovalMode(runtimeMode: RuntimeMode) {
- return runtimeMode === "full-access" ? "allowAll" : "promptUnmatched";
-}
-
-/** The `muse serve` arguments for a host: read-only hosts get no shell, writes, or workspace trust. */
-export function museServeArgs(
- options: Pick,
-) {
- const args = ["serve"];
- if (options.readOnly) {
- args.push("--disable-shell", "--disable-write");
- if (options.sessionLogging !== true) args.push("--no-session-log");
- } else {
- args.push("--trust-workspace");
- if (options.runtimeMode === "full-access") args.push("--disable-sandbox");
- }
- return args;
-}
-
-/** What T3 sends in MSP `initialize`; only full hosts ask for session MCP servers. */
-export function museInitializeParams(readOnly = false) {
- return {
- clientInfo: { name: "t3_code", title: "T3 Code", version: "1" },
- capabilities: { requestedCapabilities: readOnly ? [] : ["sessionMcp"] },
- };
-}
-
-/** One SDK-owned process. The pre-handshake handle owns cleanup even when initialize never replies. */
-export async function createMuseSdkHost(
- options: MuseSdkHostOptions,
- spawn: typeof spawnMspConnection = spawnMspConnection,
-): Promise {
- options.signal?.throwIfAborted();
- const handshake = spawn({
- command: options.binaryPath,
- args: museServeArgs(options),
- ...(options.cwd ? { cwd: options.cwd } : {}),
- // Callers pass an environment already built with makeMuseEnvironment.
- env: options.environment ?? makeMuseEnvironment(),
- // A healthy host exits at once; this only bounds a hung one on close.
- shutdownTimeoutMs: 10_000,
- });
- let closePromise: Promise | undefined;
- const close = () => {
- options.signal?.removeEventListener("abort", onAbort);
- return (closePromise ??= handshake.close().then(() => undefined));
- };
- let rejectStartup: (reason: unknown) => void = () => {};
- const onAbort = () => {
- rejectStartup(options.signal?.reason ?? new Error("Muse SDK startup aborted."));
- void close().catch(() => {});
- };
- const interrupted = new Promise((_resolve, reject) => {
- rejectStartup = reject;
- });
- options.signal?.addEventListener("abort", onAbort, { once: true });
- if (options.signal?.aborted) onAbort();
- const timer = setTimeout(
- () => rejectStartup(new Error("Muse SDK initialization timed out.")),
- options.startupTimeoutMs ?? 20_000,
- );
- timer.unref();
- try {
- const host = await Promise.race([
- handshake.initialize(museInitializeParams(options.readOnly)),
- interrupted,
- ]);
- if (host.initializeResult.schema?.version !== 1) {
- throw new Error("Muse SDK returned an unsupported protocol envelope version.");
- }
- // The SDK's fingerprint warning permits additive optional schema changes.
- // Successful initialization, not fingerprint identity, determines readiness.
- return {
- connection: host.connection,
- initializeResult: host.initializeResult,
- exited: host.exited,
- stderrTail: () => handshake.child.stderrTail,
- close,
- };
- } catch (error) {
- await close();
- throw error;
- } finally {
- clearTimeout(timer);
- }
-}
-
-/** Cancellation waits for native shutdown before callers release startup resources. */
-export const createMuseSdkHostEffect = Effect.fn("createMuseSdkHostEffect")(function* (
- options: Omit,
- createHost: typeof createMuseSdkHost = createMuseSdkHost,
-) {
- let startup: Promise | undefined;
- return yield* Effect.tryPromise((signal) => {
- startup = createHost({ ...options, signal });
- return startup;
- }).pipe(
- Effect.onInterrupt(() =>
- Effect.promise(async () => {
- await startup?.then(
- (host) => host.close(),
- () => {},
- );
- }),
- ),
- );
-});
diff --git a/apps/server/src/testUtils/fakeCli.ts b/apps/server/src/testUtils/fakeCli.ts
deleted file mode 100644
index 68ab157aed7aecb1a0419a896f67ed92cc17db7f..0000000000000000000000000000000000000000
--- a/apps/server/src/testUtils/fakeCli.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-// @effect-diagnostics nodeBuiltinImport:off preferSchemaOverJson:off - synchronous fixture writer used from plain and Effect tests alike.
-import * as NodeFS from "node:fs";
-import * as NodePath from "node:path";
-
-import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
-
-export interface FakeCliOptions {
- /** Directory the launcher and its stub are written into. */
- readonly directory: string;
- /** Command name as callers spawn it, e.g. `codex`. */
- readonly name: string;
- /** ES module source the launcher runs with `node`. `process.argv.slice(2)` carries the CLI args. */
- readonly source: string;
- /** Environment set for the stub before it runs, on top of the inherited environment. */
- readonly env?: Readonly>;
- /**
- * Platform the launcher is shaped for. Effect callers should pass the value
- * they read from `HostProcessPlatform` so an injected override is honoured;
- * defaults to the real host for plain tests.
- */
- readonly platform?: NodeJS.Platform;
-}
-
-/**
- * Writes a fake CLI whose behaviour lives in a Node stub. On posix the
- * launcher is a `#!/bin/sh` script; on Windows it is a `.cmd` shim, since a
- * shebang file is not executable there and `resolveSpawnCommand` routes
- * `.cmd` through a shell. Returns the launcher path to hand to the code under
- * test, which on Windows carries the `.cmd` extension.
- */
-export function writeFakeCli(options: FakeCliOptions): string {
- NodeFS.mkdirSync(options.directory, { recursive: true });
- const stubPath = NodePath.join(options.directory, `${options.name}-stub.mjs`);
- // The environment rides in a JSON sidecar the stub applies to itself, since
- // neither sh nor cmd.exe can quote every value (newlines, quotes, `%`) safely.
- const envPath = NodePath.join(options.directory, `${options.name}-env.json`);
- NodeFS.writeFileSync(envPath, JSON.stringify(options.env ?? {}), "utf8");
- NodeFS.writeFileSync(
- stubPath,
- [
- 'import { readFileSync as readFakeCliEnv } from "node:fs";',
- `Object.assign(process.env, JSON.parse(readFakeCliEnv(${JSON.stringify(envPath)}, "utf8")));`,
- options.source,
- ].join("\n"),
- "utf8",
- );
-
- if ((options.platform ?? HostProcessPlatform.defaultValue()) === "win32") {
- const launcherPath = NodePath.join(options.directory, `${options.name}.cmd`);
- NodeFS.writeFileSync(
- launcherPath,
- ["@echo off", `node "%~dp0${options.name}-stub.mjs" %*`, "exit /b %ERRORLEVEL%", ""].join(
- "\r\n",
- ),
- "utf8",
- );
- return launcherPath;
- }
-
- const launcherPath = NodePath.join(options.directory, options.name);
- NodeFS.writeFileSync(
- launcherPath,
- ["#!/bin/sh", `exec node "$(dirname "$0")/${options.name}-stub.mjs" "$@"`, ""].join("\n"),
- "utf8",
- );
- NodeFS.chmodSync(launcherPath, 0o755);
- return launcherPath;
-}
-
-/**
- * Stub source that becomes `scriptPath`: after optionally requiring a leading
- * argv prefix, it imports the script into its own process so the stub is the
- * agent. Nothing sits between the code under test and the mock, so a kill or
- * a closed stdin reaches it directly and its exit log is faithful on every
- * host. The script must not read `process.argv`; the mock agents are driven
- * by environment and stdin.
- */
-export function execScriptSource(options: {
- readonly scriptPath: string;
- readonly expectedArgs?: ReadonlyArray;
- /** Tab-separated argv appended here per invocation, for launch-flag assertions. */
- readonly argvLogPath?: string;
- /** Wait before handing over, for tests that race a slow startup. */
- readonly delayMs?: number;
-}): string {
- return [
- 'import { appendFileSync } from "node:fs";',
- 'import { pathToFileURL } from "node:url";',
- "const args = process.argv.slice(2);",
- ...(options.argvLogPath === undefined
- ? []
- : [
- `appendFileSync(${JSON.stringify(options.argvLogPath)}, args.join(${JSON.stringify("\t")}) + ${JSON.stringify("\n")});`,
- ]),
- ...(options.delayMs === undefined
- ? []
- : [`Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ${options.delayMs});`]),
- `const expected = ${JSON.stringify(options.expectedArgs ?? [])};`,
- "if (expected.some((value, index) => args[index] !== value)) {",
- ' process.stderr.write(`unexpected args: ${args.join(" ")}\\n`);',
- " process.exit(11);",
- "}",
- `await import(pathToFileURL(${JSON.stringify(options.scriptPath)}).href);`,
- "",
- ].join("\n");
-}
diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts
index 46e015123df24b0f9ba9335616be751cd1432d6d..29c0f950f7c895eb994ca54fa10c44524f458adb 100644
--- a/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts
+++ b/apps/server/src/textGeneration/ClaudeTextGeneration.test.ts
@@ -21,7 +21,7 @@ import {
import * as TextGeneration from "./TextGeneration.ts";
import { sanitizeThreadTitle } from "@t3tools/provider-core/server/textGenerationUtils";
import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";
-import { writeFakeCli } from "../testUtils/fakeCli.ts";
+import { writeFakeCli } from "@t3tools/provider-testing/fakeCli";
const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
const layerClaudeTextGenerationTest = ServerConfig.ServerConfig.layerTest(process.cwd(), {
diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts
index fbfbe909f962b6f90b35478bf3dd1884f97dc829..e991ddf17fdfcd1d56711b2f04adf10f750b5685 100644
--- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts
+++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts
@@ -16,7 +16,7 @@ import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools
import * as ServerConfig from "../config.ts";
import * as TextGeneration from "./TextGeneration.ts";
import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";
-import { writeFakeCli } from "../testUtils/fakeCli.ts";
+import { writeFakeCli } from "@t3tools/provider-testing/fakeCli";
const decodeCodexSettings = Schema.decodeSync(CodexSettings);
const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(
diff --git a/apps/server/src/textGeneration/GrokTextGeneration.test.ts b/apps/server/src/textGeneration/GrokTextGeneration.test.ts
index a577065913ab28bf15a2a7a6eee67614881bdd6b..8165683f62cf02a8f28884cd82bb5f2b9b1e3744 100644
--- a/apps/server/src/textGeneration/GrokTextGeneration.test.ts
+++ b/apps/server/src/textGeneration/GrokTextGeneration.test.ts
@@ -16,7 +16,7 @@ import { GrokSettings, ProviderInstanceId } from "@t3tools/contracts";
import * as ServerConfig from "../config.ts";
import * as TextGeneration from "./TextGeneration.ts";
import { makeGrokTextGeneration } from "./GrokTextGeneration.ts";
-import { execScriptSource, writeFakeCli } from "../testUtils/fakeCli.ts";
+import { execScriptSource, writeFakeCli } from "@t3tools/provider-testing/fakeCli";
const decodeGrokSettings = Schema.decodeSync(GrokSettings);
const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
diff --git a/apps/server/src/textGeneration/MuseTextGeneration.test.ts b/apps/server/src/textGeneration/MuseTextGeneration.test.ts
deleted file mode 100644
index 5f3be054870512c4a28750097df8e30ab39b4ff5..0000000000000000000000000000000000000000
--- a/apps/server/src/textGeneration/MuseTextGeneration.test.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { expect, it } from "@effect/vitest";
-import type { NotificationHandler } from "@muse-code/sdk";
-import {
- MUSE_DEFAULT_MODEL,
- MuseSettings,
- ProviderInstanceId,
- type ServerProviderModel,
-} from "@t3tools/contracts";
-import { createModelSelection } from "@t3tools/shared/model";
-import * as Effect from "effect/Effect";
-import * as Fiber from "effect/Fiber";
-import * as Schema from "effect/Schema";
-import { vi } from "vite-plus/test";
-
-import type { MuseSdkHost } from "../provider/museSdk.ts";
-import { museModelCapabilities } from "../provider/museModelCatalog.ts";
-import { makeMuseTextGeneration } from "./MuseTextGeneration.ts";
-
-const settings = Schema.decodeSync(MuseSettings)({ enabled: true });
-const modelSelection = createModelSelection(
- ProviderInstanceId.make("muse"),
- "muse-spark-1.3-contributor",
- [{ id: "reasoningEffort", value: "high" }],
-);
-const titleInput = {
- cwd: "/project/should-not-be-used",
- message: "Fix the login form",
- modelSelection,
-};
-
-function fixture(
- onTurn?: (emit: (method: string, params: Record) => void) => void,
- acknowledgeTurn = true,
- modelCatalog?: Effect.Effect>,
-) {
- let handler: NotificationHandler = () => {};
- let commandSequence = 0;
- let activeTurnId: string | undefined;
- const mintCommandId = () => `turn-${++commandSequence}`;
- const started = Promise.withResolvers();
- const closed = Promise.withResolvers();
- const emit = (method: string, params: Record) =>
- handler({
- jsonrpc: "2.0",
- method,
- params: {
- sessionId: "session-1",
- turnId: activeTurnId,
- ...params,
- ...(params.item
- ? {
- item: {
- revision: 1,
- turnId: activeTurnId,
- ...(params.item as Record),
- },
- }
- : {}),
- },
- });
- const host: MuseSdkHost = {
- initializeResult: { grantedCapabilities: [] },
- connection: {
- command: vi.fn(async (method, _params, options) => {
- // Connection.command overwrites params.commandId unless supplied in options.
- const commandId = options?.commandId ?? mintCommandId();
- if (method === "session/start") return { session: { sessionId: "session-1" } };
- if (method === "turn/start") {
- activeTurnId = commandId;
- started.resolve();
- onTurn?.(emit);
- if (!acknowledgeTurn) return await new Promise>(() => {});
- return { turnId: commandId };
- }
- return {};
- }),
- request: vi.fn(async () => ({})),
- mintCommandId,
- onNotification: (nextHandler) => {
- handler = nextHandler;
- },
- onProtocolError: () => {},
- onServerRequest: () => {},
- closed: closed.promise,
- },
- close: vi.fn(async () => {
- closed.resolve();
- }),
- exited: new Promise(() => {}),
- };
- const createHost = vi.fn(async () => host);
- const make = makeMuseTextGeneration(settings, {
- createHost,
- ...(modelCatalog ? { modelCatalog } : {}),
- });
- return {
- host,
- createHost,
- started: started.promise,
- closeConnection: closed.resolve,
- emit,
- make,
- };
-}
-
-const finish =
- (text: string) => (emit: (method: string, params: Record) => void) => {
- emit("item/updated", { item: { itemId: "answer", kind: "agentMessage", text: "partial" } });
- emit("item/completed", {
- item: { itemId: "answer", kind: "agentMessage", revision: 2, text },
- });
- emit("turn/completed", { terminal: "completed" });
- };
-
-it.layer(NodeServices.layer)("Muse text generation", (it) => {
- it.effect("validates the final item once and closes an isolated restrictive host", () =>
- Effect.gen(function* () {
- const test = fixture(finish('{"title":"Fix login form"}'));
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput)).toEqual({
- title: "Fix login form",
- });
- expect(test.createHost).toHaveBeenCalledWith(
- expect.objectContaining({ readOnly: true, sessionLogging: true }),
- );
- expect(test.createHost).not.toHaveBeenCalledWith(
- expect.objectContaining({ cwd: titleInput.cwd }),
- );
- expect(test.host.connection.command).toHaveBeenCalledWith(
- "session/start",
- expect.objectContaining({
- providerId: "meta",
- modelId: modelSelection.model,
- approvalMode: "denyUnmatched",
- }),
- );
- expect(test.host.connection.command).toHaveBeenCalledWith(
- "turn/start",
- expect.objectContaining({
- reasoningEffort: "high",
- }),
- { commandId: "turn-1" },
- );
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("uses low effort by default and normalizes stale efforts against the model", () =>
- Effect.gen(function* () {
- for (const { saved, variants, expected } of [
- { saved: undefined, variants: undefined, expected: "low" },
- { saved: undefined, variants: ["medium", "xhigh"], expected: "medium" },
- { saved: "max", variants: ["minimal", "max"], expected: "max" },
- { saved: "ultra", variants: ["medium", "xhigh"], expected: "medium" },
- { saved: "high", variants: [], expected: undefined },
- ]) {
- const test = fixture(
- finish('{"title":"Fix login form"}'),
- true,
- Effect.succeed([
- {
- slug: modelSelection.model,
- name: modelSelection.model,
- isCustom: false,
- capabilities: museModelCapabilities(variants ? { variants } : undefined),
- },
- ]),
- );
- const service = yield* test.make;
- yield* service.generateThreadTitle({
- ...titleInput,
- modelSelection: createModelSelection(
- ProviderInstanceId.make("muse"),
- modelSelection.model,
- saved !== undefined ? [{ id: "reasoningEffort", value: saved }] : undefined,
- ),
- });
- const turn = vi
- .mocked(test.host.connection.command)
- .mock.calls.find(([method]) => method === "turn/start");
- expect(turn?.[1].reasoningEffort).toBe(expected);
- expect(Object.hasOwn(turn?.[1] ?? {}, "reasoningEffort")).toBe(expected !== undefined);
- }
- }),
- );
-
- it.effect("lets Muse choose the model when none is selected", () =>
- Effect.gen(function* () {
- for (const model of [MUSE_DEFAULT_MODEL, " "]) {
- const test = fixture(finish('{"title":"Fix login form"}'));
- const service = yield* test.make;
- yield* service.generateThreadTitle({
- ...titleInput,
- modelSelection: createModelSelection(ProviderInstanceId.make("muse"), model),
- });
- const start = vi
- .mocked(test.host.connection.command)
- .mock.calls.find(([method]) => method === "session/start");
- expect(start?.[1]).not.toHaveProperty("modelId");
- }
- }),
- );
-
- it.effect("rejects invalid structured output and releases the host", () =>
- Effect.gen(function* () {
- const test = fixture(finish('{"title":42}'));
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
- _tag: "TextGenerationError",
- detail: "Muse returned invalid structured output.",
- });
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("ignores output and terminal notifications for another turn", () =>
- Effect.gen(function* () {
- const test = fixture((emit) => {
- emit("item/completed", {
- item: {
- itemId: "other-answer",
- kind: "agentMessage",
- turnId: "another-turn",
- text: '{"title":"Wrong title"}',
- },
- });
- emit("turn/completed", { turnId: "another-turn", terminal: "completed" });
- finish('{"title":"Fix login form"}')(emit);
- });
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput)).toEqual({ title: "Fix login form" });
- expect(test.host.connection.command).toHaveBeenCalledWith(
- "turn/start",
- expect.objectContaining({ sessionId: "session-1" }),
- { commandId: "turn-1" },
- );
- }),
- );
-
- it.effect("rejects a truncated response even when its prefix contains valid JSON", () =>
- Effect.gen(function* () {
- const test = fixture((emit) => {
- emit("item/completed", {
- item: {
- itemId: "answer",
- kind: "agentMessage",
- text: '{"title":"Incomplete answer"}',
- truncated: true,
- },
- });
- emit("turn/completed", { terminal: "completed" });
- });
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
- _tag: "TextGenerationError",
- });
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("settles a completed turn even when its start acknowledgement never arrives", () =>
- Effect.gen(function* () {
- const test = fixture(finish('{"title":"Fix login form"}'), false);
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput)).toEqual({ title: "Fix login form" });
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("reports a failed turn without trying another provider", () =>
- Effect.gen(function* () {
- const test = fixture((emit) =>
- emit("turn/completed", {
- terminal: "failed",
- error: { message: "Authentication required. Run muse login." },
- }),
- );
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
- _tag: "TextGenerationError",
- });
- expect(test.createHost).toHaveBeenCalledOnce();
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("settles when the transport closes before completion", () =>
- Effect.gen(function* () {
- const test = fixture();
- const service = yield* test.make;
- const pending = yield* service
- .generateThreadTitle(titleInput)
- .pipe(Effect.flip, Effect.forkChild);
- yield* Effect.promise(() => test.started);
- test.closeConnection();
- expect(yield* Fiber.join(pending)).toMatchObject({ _tag: "TextGenerationError" });
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("releases the host when interrupted after the turn starts", () =>
- Effect.gen(function* () {
- const test = fixture();
- const service = yield* test.make;
- const fiber = yield* service.generateThreadTitle(titleInput).pipe(Effect.forkChild);
- yield* Effect.promise(() => test.started);
- yield* Fiber.interrupt(fiber);
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("denies approval requests and fails the noninteractive request", () =>
- Effect.gen(function* () {
- const test = fixture((emit) =>
- emit("approval/requested", {
- approvalId: "approval-1",
- currentRequirementId: "requirement-1",
- availableChoices: [{ choiceId: "deny-1", decision: "denied" }],
- }),
- );
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
- _tag: "TextGenerationError",
- });
- expect(test.host.connection.command).toHaveBeenCalledWith("approval/decide", {
- sessionId: "session-1",
- approvalId: "approval-1",
- requirementId: "requirement-1",
- choiceId: "deny-1",
- });
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("cancels user input requests", () =>
- Effect.gen(function* () {
- const test = fixture((emit) => emit("userInput/requested", { userInputId: "input-1" }));
- const service = yield* test.make;
- expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
- _tag: "TextGenerationError",
- });
- expect(test.host.connection.command).toHaveBeenCalledWith(
- "userInput/cancel",
- expect.objectContaining({
- userInputId: "input-1",
- }),
- );
- expect(test.host.close).toHaveBeenCalledOnce();
- }),
- );
-
- it.effect("generates and sanitizes commit content through Muse", () =>
- Effect.gen(function* () {
- const test = fixture(
- finish('{"subject":"Fix login.","body":" Details ","branch":"fix-login"}'),
- );
- const service = yield* test.make;
- const generated = yield* service.generateCommitMessage({
- cwd: titleInput.cwd,
- branch: "main",
- stagedSummary: "login.ts",
- stagedPatch: "login change",
- includeBranch: true,
- modelSelection,
- });
- expect(generated.subject).toBe("Fix login");
- expect(generated.body).toBe("Details");
- expect(generated.branch).toContain("fix-login");
- }),
- );
-});
diff --git a/apps/server/src/textGeneration/MuseTextGeneration.ts b/apps/server/src/textGeneration/MuseTextGeneration.ts
deleted file mode 100644
index e0d4f3f3afa519ead68c1d325083c1e89146d4e5..0000000000000000000000000000000000000000
--- a/apps/server/src/textGeneration/MuseTextGeneration.ts
+++ /dev/null
@@ -1,350 +0,0 @@
-import {
- DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
- MUSE_DEFAULT_MODEL,
- type ModelSelection,
- type MuseSettings,
- type ServerProviderModel,
- TextGenerationError,
-} from "@t3tools/contracts";
-import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
-import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
-import { extractJsonObject } from "@t3tools/shared/schemaJson";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Option from "effect/Option";
-import * as Schema from "effect/Schema";
-
-import {
- createMuseSdkHost,
- createMuseSdkHostEffect,
- type MuseSdkHost,
-} from "../provider/museSdk.ts";
-import { museModelCapabilities, resolveMuseReasoningEffort } from "../provider/museModelCatalog.ts";
-import type * as TextGeneration from "./TextGeneration.ts";
-import {
- buildBranchNamePrompt,
- buildCommitMessagePrompt,
- buildPrContentPrompt,
- buildThreadTitlePrompt,
-} from "@t3tools/provider-core/server/textGenerationPrompts";
-import {
- sanitizeCommitSubject,
- sanitizePrTitle,
- sanitizeThreadTitle,
- toJsonSchemaObject,
-} from "@t3tools/provider-core/server/textGenerationUtils";
-
-const SessionStarted = Schema.Struct({ session: Schema.Struct({ sessionId: Schema.String }) });
-const ItemNotification = Schema.Struct({
- sessionId: Schema.String,
- item: Schema.Struct({
- itemId: Schema.String,
- kind: Schema.String,
- revision: Schema.Int,
- turnId: Schema.optionalKey(Schema.String),
- text: Schema.optionalKey(Schema.String),
- retracted: Schema.optionalKey(Schema.Boolean),
- truncated: Schema.optionalKey(Schema.Boolean),
- }),
-});
-const TurnCompleted = Schema.Struct({
- sessionId: Schema.String,
- turnId: Schema.String,
- terminal: Schema.String,
- error: Schema.optionalKey(Schema.Struct({ message: Schema.String })),
-});
-const ApprovalRequested = Schema.Struct({
- sessionId: Schema.String,
- approvalId: Schema.String,
- currentRequirementId: Schema.Unknown,
- availableChoices: Schema.Array(
- Schema.Struct({ choiceId: Schema.String, decision: Schema.String }),
- ),
-});
-const UserInputRequested = Schema.Struct({
- sessionId: Schema.String,
- userInputId: Schema.String,
-});
-const decodeSession = Schema.decodeUnknownSync(SessionStarted);
-const decodeItem = Schema.decodeUnknownSync(ItemNotification);
-const decodeCompleted = Schema.decodeUnknownSync(TurnCompleted);
-const decodeApproval = Schema.decodeUnknownSync(ApprovalRequested);
-const decodeUserInput = Schema.decodeUnknownSync(UserInputRequested);
-const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
-const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown));
-
-type Operation = keyof TextGeneration.TextGeneration["Service"];
-
-async function generateMuseText(
- host: MuseSdkHost,
- workspaceRoot: string,
- prompt: string,
- modelId: string | undefined,
- reasoningEffort: string | undefined,
- signal: AbortSignal,
-) {
- let sessionId: string | undefined;
- const turnId = host.connection.mintCommandId();
- const messages = new Map();
- let resolveCompletion: (text: string) => void = () => {};
- let rejectCompletion: (error: unknown) => void = () => {};
- const completion = new Promise((resolve, reject) => {
- resolveCompletion = resolve;
- rejectCompletion = reject;
- });
- // Notifications may settle the turn before the turn/start acknowledgement arrives.
- void completion.catch(() => {});
- const rejectInteractive = async (method: string, params: Record) => {
- if (method === "approval/requested") {
- const approval = decodeApproval(params);
- if (approval.sessionId !== sessionId) return;
- rejectCompletion(new Error("Muse requested an interactive action during text generation."));
- const deny = approval.availableChoices.find((choice) => choice.decision === "denied");
- if (deny) {
- await host.connection.command("approval/decide", {
- sessionId,
- approvalId: approval.approvalId,
- requirementId: approval.currentRequirementId,
- choiceId: deny.choiceId,
- });
- }
- } else {
- const request = decodeUserInput(params);
- if (request.sessionId !== sessionId) return;
- rejectCompletion(new Error("Muse requested an interactive action during text generation."));
- await host.connection.command("userInput/cancel", {
- sessionId,
- userInputId: request.userInputId,
- reason: "T3 Code text generation cannot request user input.",
- });
- }
- };
- host.connection.onNotification((notification) => {
- try {
- if (notification.method === "item/completed" || notification.method === "item/updated") {
- const { item, sessionId: itemSessionId } = decodeItem(notification.params);
- if (itemSessionId !== sessionId || item.turnId !== turnId || item.kind !== "agentMessage")
- return;
- const previous = messages.get(item.itemId);
- if (!previous || item.revision > previous.revision) messages.set(item.itemId, item);
- } else if (notification.method === "turn/completed") {
- const completed = decodeCompleted(notification.params);
- if (completed.sessionId !== sessionId || completed.turnId !== turnId) return;
- if (completed.terminal === "completed") {
- // A turn may include commentary before its final structured answer.
- const answer = [...messages.values()].findLast((item) => !item.retracted);
- if (answer?.truncated) {
- rejectCompletion(new Error("Muse text generation returned a truncated response."));
- } else {
- resolveCompletion(answer?.text ?? "");
- }
- } else {
- rejectCompletion(
- new Error(completed.error?.message ?? `Muse turn ${completed.terminal}.`),
- );
- }
- } else if (
- notification.method === "approval/requested" ||
- notification.method === "userInput/requested"
- ) {
- void rejectInteractive(notification.method, notification.params ?? {}).catch(
- rejectCompletion,
- );
- } else if (notification.method === "view/gap") {
- rejectCompletion(new Error("Muse text generation lost part of its response."));
- }
- } catch (error) {
- rejectCompletion(error);
- }
- });
- host.connection.onProtocolError(rejectCompletion);
- host.connection.onServerRequest(async () => {
- const error = new Error("T3 Code text generation cannot approve interactive requests.");
- rejectCompletion(error);
- throw error;
- });
- void host.connection.closed.then(() => {
- rejectCompletion(new Error("Muse connection closed before text generation completed."));
- });
- void host.exited.then(() => {
- rejectCompletion(new Error("Muse exited before text generation completed."));
- });
- const onAbort = () => rejectCompletion(signal.reason);
- signal.addEventListener("abort", onAbort, { once: true });
- try {
- signal.throwIfAborted();
- const started = decodeSession(
- await host.connection.command("session/start", {
- workspaceRoot,
- providerId: "meta",
- ...(modelId ? { modelId } : {}),
- approvalMode: "denyUnmatched",
- }),
- );
- sessionId = started.session.sessionId;
- const acknowledged = host.connection.command(
- "turn/start",
- {
- sessionId,
- input: [{ type: "text", text: prompt }],
- ...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
- },
- { commandId: turnId },
- );
- return await Promise.race([acknowledged.then(() => completion), completion]);
- } finally {
- signal.removeEventListener("abort", onAbort);
- }
-}
-
-export const makeMuseTextGeneration = Effect.fn("makeMuseTextGeneration")(function* (
- settings: MuseSettings,
- options: {
- readonly environment?: NodeJS.ProcessEnv;
- readonly modelCatalog?: Effect.Effect>;
- /** Test hook. */
- readonly createHost?: typeof createMuseSdkHost;
- } = {},
-) {
- const { environment, modelCatalog = Effect.succeed([]), createHost } = options;
- const fileSystem = yield* FileSystem.FileSystem;
- const runMuseJson = Effect.fn("runMuseJson")(function* (input: {
- operation: Operation;
- prompt: string;
- outputSchema: S;
- modelSelection: ModelSelection;
- }): Effect.fn.Return {
- const { operation } = input;
- if (!settings.enabled) {
- return yield* new TextGenerationError({ operation, detail: "Muse Code is disabled." });
- }
- const selectedModel = input.modelSelection.model.trim();
- const models = yield* modelCatalog;
- // "default" is the catalog's default model, as in chat. Without one, Muse picks.
- const catalogModel =
- selectedModel && selectedModel !== MUSE_DEFAULT_MODEL
- ? models.find((model) => model.slug === selectedModel)
- : models.find((model) => model.isDefault && !model.isCustom);
- const modelId =
- selectedModel && selectedModel !== MUSE_DEFAULT_MODEL ? selectedModel : catalogModel?.slug;
- const reasoningEffort = resolveMuseReasoningEffort(
- catalogModel?.capabilities ?? museModelCapabilities(),
- getModelSelectionStringOptionValue(input.modelSelection, "reasoningEffort") ??
- DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
- );
- const jsonSchema = yield* encodeJson(toJsonSchemaObject(input.outputSchema)).pipe(
- Effect.mapError(
- (cause) => new TextGenerationError({ operation, detail: "Invalid output schema.", cause }),
- ),
- );
- const response = yield* Effect.gen(function* () {
- // Metadata requests already include their context and do not need checkout configuration.
- const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-muse-text-" });
- const host = yield* Effect.acquireRelease(
- createMuseSdkHostEffect(
- {
- binaryPath: settings.binaryPath,
- cwd,
- ...(environment ? { environment } : {}),
- readOnly: true,
- // Muse derives SDK turn events from its session log, including completion.
- sessionLogging: true,
- },
- createHost,
- ),
- (host: MuseSdkHost) => Effect.promise(() => host.close()),
- { interruptible: true },
- );
- return yield* Effect.tryPromise((signal) =>
- generateMuseText(
- host,
- cwd,
- `${input.prompt}\n\nReturn only a JSON object matching this schema, with no markdown fences. Do not use tools or ask questions.\n${jsonSchema}`,
- modelId,
- reasoningEffort,
- signal,
- ),
- );
- }).pipe(
- Effect.scoped,
- Effect.mapError(
- (cause) =>
- new TextGenerationError({
- operation,
- detail:
- "Muse Code text generation failed. Check Muse login and availability on this T3 server host.",
- cause,
- }),
- ),
- Effect.timeoutOption(180_000),
- Effect.flatMap(
- Option.match({
- onNone: () =>
- Effect.fail(new TextGenerationError({ operation, detail: "Muse request timed out." })),
- onSome: Effect.succeed,
- }),
- ),
- );
- const decodeOutput = Schema.decodeEffect(input.outputSchema);
- return yield* decodeJson(extractJsonObject(response)).pipe(
- Effect.flatMap(decodeOutput),
- Effect.mapError(
- (cause) =>
- new TextGenerationError({
- operation,
- detail: "Muse returned invalid structured output.",
- cause,
- }),
- ),
- );
- });
-
- const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =
- Effect.fn("MuseTextGeneration.generateCommitMessage")(function* (input) {
- const generated = yield* runMuseJson({
- operation: "generateCommitMessage",
- ...buildCommitMessagePrompt({ ...input, includeBranch: input.includeBranch === true }),
- modelSelection: input.modelSelection,
- });
- return {
- subject: sanitizeCommitSubject(generated.subject),
- body: generated.body.trim(),
- ...("branch" in generated && typeof generated.branch === "string"
- ? { branch: sanitizeFeatureBranchName(generated.branch) }
- : {}),
- };
- });
- const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =
- Effect.fn("MuseTextGeneration.generatePrContent")(function* (input) {
- const generated = yield* runMuseJson({
- operation: "generatePrContent",
- ...buildPrContentPrompt(input),
- modelSelection: input.modelSelection,
- });
- return { title: sanitizePrTitle(generated.title), body: generated.body.trim() };
- });
- const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =
- Effect.fn("MuseTextGeneration.generateBranchName")(function* (input) {
- const generated = yield* runMuseJson({
- operation: "generateBranchName",
- ...buildBranchNamePrompt(input),
- modelSelection: input.modelSelection,
- });
- return { branch: sanitizeBranchFragment(generated.branch) };
- });
- const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =
- Effect.fn("MuseTextGeneration.generateThreadTitle")(function* (input) {
- const generated = yield* runMuseJson({
- operation: "generateThreadTitle",
- ...buildThreadTitlePrompt(input),
- modelSelection: input.modelSelection,
- });
- return { title: sanitizeThreadTitle(generated.title) };
- });
- return {
- generateCommitMessage,
- generatePrContent,
- generateBranchName,
- generateThreadTitle,
- } satisfies TextGeneration.TextGeneration["Service"];
-});
diff --git a/apps/web/package.json b/apps/web/package.json
index ab2ae7b35ea32e757b08df5adf9fd1506b0b12cd..63a5f5c2ad357b62aa031ba5a4f78b563a9a1c7b 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -29,6 +29,7 @@
"@t3tools/client-runtime": "workspace:*",
"@t3tools/contracts": "workspace:*",
"@t3tools/provider-core": "workspace:*",
+ "@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
"@t3tools/shared": "workspace:*",
"@tanstack/react-pacer": "^0.19.4",
diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx
index 66e170dacf61bf3a93f88f2336ed7fdc03f01882..7f38d65f2a14e426134172d89be89d8cc4778193 100644
--- a/apps/web/src/components/Icons.tsx
+++ b/apps/web/src/components/Icons.tsx
@@ -815,13 +815,3 @@ export const ComputerUseAppIcon: Icon = (props) => {
);
};
-
-export const MuseIcon: Icon = (props) => (
-
-);
diff --git a/apps/web/src/components/chat/ProviderInstanceIcon.tsx b/apps/web/src/components/chat/ProviderInstanceIcon.tsx
index ddfffd4fa9d3b6145755643cf66000ca8a5cca17..e9e54575c9103ffa5ae296bce6a4897544205de1 100644
--- a/apps/web/src/components/chat/ProviderInstanceIcon.tsx
+++ b/apps/web/src/components/chat/ProviderInstanceIcon.tsx
@@ -8,7 +8,6 @@ import {
ClaudeAI,
CursorIcon,
GrokIcon,
- MuseIcon,
Icon,
OpenAI,
OpenCodeIcon,
@@ -29,7 +28,6 @@ const PROVIDER_ICON_BY_PROVIDER: Partial> = {
[ProviderDriverKind.make("opencode")]: OpenCodeIcon,
[ProviderDriverKind.make("cursor")]: CursorIcon,
[ProviderDriverKind.make("grok")]: GrokIcon,
- [ProviderDriverKind.make("muse")]: MuseIcon,
[ProviderDriverKind.make("antigravity")]: AntigravityIcon,
};
diff --git a/apps/web/src/components/settings/providerDriverMeta.ts b/apps/web/src/components/settings/providerDriverMeta.ts
index 59b42d0f6751bc3c17286d48d83b0024d0e1c42c..c967100888c85aa05f7eab4b1edd47a1ea8ac0d7 100644
--- a/apps/web/src/components/settings/providerDriverMeta.ts
+++ b/apps/web/src/components/settings/providerDriverMeta.ts
@@ -6,10 +6,10 @@ import {
CursorSettings,
GrokSettings,
OpenCodeSettings,
- MuseSettings,
ProviderDriverKind,
} from "@t3tools/contracts";
import { makeProviderClientRegistry } from "@t3tools/provider-core/client";
+import { museClient } from "@t3tools/provider-muse/client";
import { piClient } from "@t3tools/provider-pi/client";
/** The provider client definitions this web build ships, in presentation order. */
@@ -53,12 +53,7 @@ export const providerClients = makeProviderClientRegistry([
label: "Antigravity",
settingsSchema: AntigravitySettings,
},
- {
- driverKind: ProviderDriverKind.make("muse"),
- label: "Muse Code",
- settingsSchema: MuseSettings,
- badgeLabel: "Beta",
- },
+ museClient,
piClient,
{
driverKind: ProviderDriverKind.make("acpRegistry"),
diff --git a/package.json b/package.json
index eece70e89ef5e0f61f902356bcd0b83e1f3e526e..a345b307273db1b89d716eb8830e526229d88d9e 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"tc": "vp run -r --concurrency-limit 2 typecheck",
"lint": "vp lint --report-unused-disable-directives",
"knip": "knip --preprocessor ./scripts/knip-schemas.ts",
- "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-testing --workspace packages/provider-pi --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints",
+ "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints",
"knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts",
"lint:mobile": "node scripts/mobile-native-static-check.ts",
"test": "vp run -r test",
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index c9b3c00136dc750fa598ece6acd644bec38390f8..e62f4ff578b791a07ef161e328c00bbf02c57ccc 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -840,29 +840,6 @@ export const AntigravitySettings = makeProviderSettingsSchema(
);
export type AntigravitySettings = typeof AntigravitySettings.Type;
-export const MuseSettings = makeProviderSettingsSchema(
- {
- enabled: Schema.Boolean.pipe(
- Schema.withDecodingDefault(Effect.succeed(false)),
- Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
- ),
- binaryPath: makeBinaryPathSetting("muse").pipe(
- Schema.annotateKey({
- title: "Binary path",
- description:
- "Path to the Muse Code binary on this environment. Sign in with muse login on that host.",
- providerSettingsForm: { placeholder: "muse", clearWhenEmpty: "omit" },
- }),
- ),
- customModels: Schema.Array(CustomModelSetting).pipe(
- Schema.withDecodingDefault(Effect.succeed([])),
- Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
- ),
- },
- { order: ["binaryPath"] },
-);
-export type MuseSettings = typeof MuseSettings.Type;
-
export const AcpRegistryDistributionPreference = Schema.Literals(["auto", "binary", "npx", "uvx"]);
export type AcpRegistryDistributionPreference = typeof AcpRegistryDistributionPreference.Type;
diff --git a/packages/provider-muse/package.json b/packages/provider-muse/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..a59c6cd20c8dfe3d5eb1b47669972efb45e3694c
--- /dev/null
+++ b/packages/provider-muse/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@t3tools/provider-muse",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./client": {
+ "types": "./src/client.ts",
+ "import": "./src/client.ts"
+ },
+ "./settings": {
+ "types": "./src/settings.ts",
+ "import": "./src/settings.ts"
+ },
+ "./server": {
+ "types": "./src/server.ts",
+ "import": "./src/server.ts"
+ },
+ "./testing": {
+ "types": "./src/testing.ts",
+ "import": "./src/testing.ts"
+ }
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit",
+ "test": "vp test run --config ../../vite.config.ts --dir ."
+ },
+ "dependencies": {
+ "@muse-code/sdk": "1.4.2",
+ "@t3tools/contracts": "workspace:*",
+ "@t3tools/provider-core": "workspace:*",
+ "@t3tools/shared": "workspace:*",
+ "effect": "catalog:"
+ },
+ "devDependencies": {
+ "@effect/platform-node": "catalog:",
+ "@effect/vitest": "catalog:",
+ "@t3tools/provider-testing": "workspace:*",
+ "@types/node": "catalog:",
+ "vite-plus": "catalog:"
+ }
+}
diff --git a/packages/provider-muse/src/client.ts b/packages/provider-muse/src/client.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d709e86893955be24f259969bed0aa54b0e37af7
--- /dev/null
+++ b/packages/provider-muse/src/client.ts
@@ -0,0 +1,27 @@
+/**
+ * Muse Code's client definition: settings form, label, badge, and glyph.
+ * Browser- and React Native-safe.
+ *
+ * @module provider-muse/client
+ */
+import { ProviderDriverKind } from "@t3tools/contracts";
+import { defineProviderClient } from "@t3tools/provider-core/client";
+
+import { MuseSettings } from "./settings.ts";
+
+export const museClient = defineProviderClient({
+ driverKind: ProviderDriverKind.make("muse"),
+ label: "Muse Code",
+ settingsSchema: MuseSettings,
+ badgeLabel: "Beta",
+ icon: {
+ viewBox: "0 0 150.4828 100",
+ fill: { light: "#171717", dark: "#e5e5e5" },
+ paths: [
+ {
+ d: "M107.9999847,0.0000013c-12.3391113,0-21.984787,9.2935991-30.7164001,21.0993118 C65.2847366,5.821527,55.2499847,0,43.2413788,0C18.7586193,0,0,31.8620682,0,65.5862122 C0,86.6896515,10.2095623,100,27.3103466,100c12.3080502,0,21.1600361-5.8025894,36.8965454-33.3103409 c0,0,6.5597382-11.5841255,11.0725021-19.563858c1.5813522,2.5531845,3.2431107,5.3001404,4.9964676,8.2535095 l7.379303,12.4137955C102.0298843,91.8478851,110.039032,100,124.5517273,100 c16.6595764,0,25.9310303-13.4923325,25.9310303-35.0344849 C150.4827576,29.6551723,131.301178,0.0000013,107.9999847,0.0000013z M52.2068977,59.2413788 c-12.7586212,20-17.1724167,24.4827576-24.2758656,24.4827576c-7.3103428,0-11.6551704-6.4178314-11.6551704-17.8620682 c0-24.4827576,12.2068977-49.5172424,26.7586231-49.5172424c7.880127,0,14.4653816,4.5510426,24.5522308,18.9913158 C58.0087967,50.0272293,52.2068977,59.2413788,52.2068977,59.2413788z M100.3597336,56.7235756L91.5367355,42.00877 c-2.3876724-3.8832092-4.6757278-7.451458-6.8838348-10.7230263 c7.951973-12.273531,14.5113449-18.3891907,22.3126144-18.3891907c16.2069016,0,29.1724243,23.8620682,29.1724243,53.1724091 c0,11.1724167-3.6593475,17.6551743-11.2413864,17.6551743 C117.6294937,83.7241364,114.1578674,78.924736,100.3597336,56.7235756z",
+ fillRule: "evenodd",
+ },
+ ],
+ },
+});
diff --git a/packages/provider-muse/src/server.ts b/packages/provider-muse/src/server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7100802d7e13d8a0a7053a7695c33fa61f4987bc
--- /dev/null
+++ b/packages/provider-muse/src/server.ts
@@ -0,0 +1,8 @@
+/**
+ * Muse Code's server entry: the driver the server registers, plus the
+ * adapter factory the replay testkit and fixture recorder build on.
+ *
+ * @module provider-muse/server
+ */
+export { MuseDriver, type MuseDriverEnv } from "./server/driver.ts";
+export { makeMuseAdapterV2, type MuseAdapterV2Options } from "./server/adapter.ts";
diff --git a/packages/provider-muse/src/server/adapter.test.ts b/packages/provider-muse/src/server/adapter.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..59f8adec402682450152bb3d07834ee26623b784
--- /dev/null
+++ b/packages/provider-muse/src/server/adapter.test.ts
@@ -0,0 +1,1107 @@
+import { assert, describe, it } from "@effect/vitest";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { MspError } from "@muse-code/sdk";
+import {
+ MUSE_DEFAULT_MODEL,
+ EnvironmentId,
+ MessageId,
+ NodeId,
+ ProjectId,
+ ProviderInstanceId,
+ ProviderDriverKind,
+ ProviderSessionId,
+ ProviderThreadId,
+ ProviderTurnId,
+ RunAttemptId,
+ RunId,
+ ThreadId,
+ type OrchestrationV2AppThread,
+ type OrchestrationV2ProviderThread,
+} from "@t3tools/contracts";
+import { MuseSettings } from "../settings.ts";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import type * as Exit from "effect/Exit";
+import * as FileSystem from "effect/FileSystem";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Queue from "effect/Queue";
+import * as Schema from "effect/Schema";
+import * as Stream from "effect/Stream";
+
+import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost";
+import { layerTestProviderHost } from "@t3tools/provider-testing/host";
+import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
+import type { MuseItem } from "./protocol.ts";
+import type { MuseSdkHost } from "./sdk.ts";
+import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
+import {
+ ProviderAdapterV2RuntimePolicy,
+ type ProviderAdapterV2Event,
+ type ProviderAdapterV2TurnInput,
+} from "@t3tools/provider-core/server/ProviderAdapter";
+import type { ProviderContinuationRequest } from "@t3tools/provider-core/server/continuationRequests";
+import { makeMuseAdapterV2, type MuseAdapterV2Options } from "./adapter.ts";
+
+const testLayer = Layer.mergeAll(
+ NodeServices.layer,
+ IdAllocator.layer,
+ layerTestProviderHost().pipe(Layer.provide(NodeServices.layer)),
+);
+const MUSE_PROVIDER = ProviderDriverKind.make("muse");
+const INSTANCE_ID = ProviderInstanceId.make("muse_work");
+const THREAD_ID = ThreadId.make("thread-muse-test");
+const MODEL = "muse-spark-1.3-contributor";
+const museSettings = Schema.decodeSync(MuseSettings)({ enabled: true });
+const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
+ runtimeMode: "full-access",
+ interactionMode: "default",
+ cwd: null,
+});
+const modelSelection = (instanceId = INSTANCE_ID) => ({
+ instanceId,
+ model: MODEL,
+ options: [{ id: "reasoningEffort", value: "max" }],
+});
+
+interface NativeCall {
+ readonly method: string;
+ readonly params: Record;
+ readonly commandId?: string;
+}
+
+function pendingPromise() {
+ let resolve!: (value: A) => void;
+ let reject!: (reason: unknown) => void;
+ const promise = new Promise((done, fail) => {
+ resolve = done;
+ reject = fail;
+ });
+ return { promise, resolve, reject };
+}
+
+/** SDK-shaped transport fake: registration deliberately returns void, as the SDK does. */
+const makeFakeMuse = Effect.fnUntraced(function* (idPrefix = "native") {
+ const requests = yield* Queue.unbounded();
+ const calls: NativeCall[] = [];
+ const closed = pendingPromise();
+ const exited = pendingPromise>();
+ let notify: Parameters[0] = () => {};
+ let sequence = 0;
+ let sessionId = "";
+ let closeCount = 0;
+ let closeGate: Promise | undefined;
+ let history: MuseItem[] = [];
+ const responses = new Map<
+ string,
+ Array> | Record | Error>
+ >();
+ const respond = async (call: NativeCall): Promise> => {
+ calls.push(call);
+ Queue.offerUnsafe(requests, call);
+ if (["session/start", "session/resume", "session/read"].includes(call.method)) {
+ sessionId = String(call.params.sessionId);
+ }
+ const queued = responses.get(call.method)?.shift();
+ if (queued instanceof Error) throw queued;
+ if (queued) return queued;
+ switch (call.method) {
+ case "session/start":
+ case "session/resume":
+ case "session/read":
+ sessionId = String(call.params.sessionId);
+ return {
+ session: { sessionId, modelId: MODEL, activeTurnId: null },
+ history: { items: history },
+ };
+ case "turn/start":
+ return { turnId: call.commandId };
+ case "turn/steer":
+ return { turnId: call.params.expectedTurnId };
+ case "session/compact":
+ return { status: "accepted" };
+ default:
+ return {};
+ }
+ };
+ const host: MuseSdkHost = {
+ connection: {
+ mintCommandId: () => `${idPrefix}-${++sequence}`,
+ command: (method, params, options) =>
+ respond({
+ method,
+ params,
+ ...(options?.commandId ? { commandId: options.commandId } : {}),
+ }),
+ request: (method, params = {}) => respond({ method, params }),
+ onNotification: (handler) => {
+ notify = handler;
+ },
+ onProtocolError: () => {},
+ onServerRequest: () => {},
+ closed: closed.promise,
+ },
+ initializeResult: { grantedCapabilities: [] },
+ exited: exited.promise,
+ close: async () => {
+ closeCount += 1;
+ await closeGate;
+ closed.resolve();
+ exited.resolve({ code: 0, signal: null });
+ },
+ };
+ return {
+ host,
+ calls,
+ closeCount: () => closeCount,
+ holdClose: (gate: Promise) => {
+ closeGate = gate;
+ },
+ disconnect: () =>
+ Effect.sync(() => {
+ closed.resolve();
+ exited.resolve({ code: 1, signal: null });
+ }),
+ setHistory: (items: MuseItem[]) => {
+ history = items;
+ },
+ queueResponse: (
+ method: string,
+ result: Record | Promise> | Error,
+ ) => {
+ responses.set(method, [...(responses.get(method) ?? []), result]);
+ },
+ emit: (method: string, params: Record) =>
+ Effect.sync(() => {
+ notify({ jsonrpc: "2.0", method, params: { sessionId, ...params } });
+ }),
+ takeCall: Effect.fnUntraced(function* (method: string) {
+ while (true) {
+ const call = yield* Queue.take(requests);
+ if (call.method === method) return call;
+ }
+ }),
+ };
+});
+
+const makeHarness = Effect.fnUntraced(function* (
+ fake: Effect.Success>,
+ instanceId = INSTANCE_ID,
+ initialNativeThreadId?: string,
+ replacement?: Effect.Success>,
+ existingProviderThread?: OrchestrationV2ProviderThread,
+ policy = runtimePolicy,
+ overrides: Pick<
+ MuseAdapterV2Options,
+ "createHost" | "nativeEventLogger" | "modelCatalog" | "continuationRequests"
+ > = {},
+) {
+ let hostCount = 0;
+ const adapter = makeMuseAdapterV2({
+ instanceId,
+ settings: museSettings,
+ environment: { PATH: "/fake/bin" },
+ idAllocator: yield* IdAllocator.IdAllocatorV2,
+ host: yield* ProviderHost,
+ fileSystem: yield* FileSystem.FileSystem,
+ createHost: async () => (hostCount++ === 0 ? fake.host : (replacement ?? fake).host),
+ ...overrides,
+ });
+ const runtime = yield* adapter.openSession({
+ threadId: THREAD_ID,
+ providerSessionId: ProviderSessionId.make(`session-${instanceId}`),
+ modelSelection: modelSelection(instanceId),
+ runtimePolicy: policy,
+ ...(initialNativeThreadId ? { initialNativeThreadId } : {}),
+ });
+ const emitted = yield* Queue.unbounded();
+ const allEvents: ProviderAdapterV2Event[] = [];
+ const eventsEnded = yield* Deferred.make>>();
+ yield* runtime.events.pipe(
+ Stream.runForEach((event) =>
+ Effect.gen(function* () {
+ allEvents.push(event);
+ yield* Queue.offer(emitted, event);
+ }),
+ ),
+ Effect.exit,
+ Effect.flatMap((exit) => Deferred.succeed(eventsEnded, exit)),
+ Effect.forkScoped,
+ );
+ const providerThread = yield* runtime.ensureThread({
+ threadId: THREAD_ID,
+ modelSelection: modelSelection(instanceId),
+ runtimePolicy: policy,
+ ...(existingProviderThread ? { existingProviderThread } : {}),
+ });
+ const takeEvent = Effect.fnUntraced(function* (
+ type: T,
+ predicate: (event: Extract) => boolean = () => true,
+ ) {
+ while (true) {
+ const event = yield* Queue.take(emitted);
+ if (event.type === type && predicate(event as Extract)) {
+ return event as Extract;
+ }
+ }
+ });
+ return { adapter, runtime, providerThread, takeEvent, allEvents, policy, eventsEnded };
+});
+
+const preallocatedProviderThread = Effect.fnUntraced(function* () {
+ const now = yield* DateTime.now;
+ return {
+ id: ProviderThreadId.make("preallocated-muse-thread"),
+ driver: MUSE_PROVIDER,
+ providerInstanceId: INSTANCE_ID,
+ providerSessionId: ProviderSessionId.make(`session-${INSTANCE_ID}`),
+ appThreadId: THREAD_ID,
+ ownerNodeId: null,
+ nativeThreadRef: null,
+ nativeConversationHeadRef: null,
+ status: "idle",
+ firstRunOrdinal: null,
+ lastRunOrdinal: null,
+ handoffIds: [],
+ forkedFrom: null,
+ createdAt: now,
+ updatedAt: now,
+ } satisfies OrchestrationV2ProviderThread;
+});
+
+const turnInput = Effect.fnUntraced(function* (
+ providerThread: OrchestrationV2ProviderThread,
+ runOrdinal = 1,
+ policy = runtimePolicy,
+): Effect.fn.Return {
+ const now = yield* DateTime.now;
+ const threadId = providerThread.appThreadId ?? THREAD_ID;
+ const selection = modelSelection(providerThread.providerInstanceId);
+ const appThread: OrchestrationV2AppThread = {
+ id: threadId,
+ projectId: ProjectId.make("project-muse-test"),
+ title: "Muse test",
+ createdBy: "user",
+ creationSource: "web",
+ providerInstanceId: providerThread.providerInstanceId,
+ modelSelection: selection,
+ runtimeMode: policy.runtimeMode,
+ interactionMode: policy.interactionMode,
+ branch: null,
+ worktreePath: null,
+ activeProviderThreadId: providerThread.id,
+ lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
+ forkedFrom: null,
+ createdAt: now,
+ updatedAt: now,
+ archivedAt: null,
+ settledOverride: null,
+ settledAt: null,
+ lastVisitedAt: null,
+ deletedAt: null,
+ };
+ return {
+ appThread,
+ threadId,
+ runId: RunId.make(`run-${runOrdinal}`),
+ runOrdinal,
+ providerTurnOrdinal: runOrdinal,
+ attemptId: RunAttemptId.make(`attempt-${runOrdinal}`),
+ rootNodeId: NodeId.make(`root-${runOrdinal}`),
+ providerThread,
+ message: {
+ messageId: MessageId.make(`message-${runOrdinal}`),
+ text: "Hello Muse",
+ attachments: [],
+ createdBy: "user",
+ creationSource: "web",
+ },
+ modelSelection: selection,
+ runtimePolicy: policy,
+ };
+});
+
+const startConversation = Effect.fnUntraced(function* (
+ harness: Effect.Success>,
+ fake: Effect.Success>,
+ ordinal = 1,
+) {
+ yield* harness.runtime.startTurn(
+ yield* turnInput(harness.providerThread, ordinal, harness.policy),
+ );
+ const call = yield* fake.takeCall("turn/start");
+ const event = yield* harness.takeEvent(
+ "provider_turn.updated",
+ (event) => event.providerTurn.status === "running",
+ );
+ return { nativeId: call.commandId!, providerTurn: event.providerTurn };
+});
+
+const approval = (turnId: string) => ({
+ approvalId: "approval-1",
+ turnId,
+ currentRequirementId: { approvalId: "approval-1", sourceIndex: 0 },
+ availableChoices: [
+ { choiceId: "once", label: "Allow once", decision: "approved", scope: "once" },
+ { choiceId: "deny", label: "Deny", decision: "denied", scope: "once" },
+ ],
+ subject: { kind: "shell", command: "touch result.txt" },
+});
+
+describe("MuseAdapterV2", () => {
+ it.effect("connects the thread's MCP credential on native start and resume", () =>
+ Effect.gen(function* () {
+ yield* Effect.addFinalizer(() =>
+ Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID)),
+ );
+ McpProviderSession.setMcpProviderSession({
+ environmentId: EnvironmentId.make("test-environment"),
+ threadId: THREAD_ID,
+ providerSessionId: "test-session",
+ providerInstanceId: INSTANCE_ID,
+ endpoint: "http://127.0.0.1:43210/mcp",
+ authorizationHeader: "Bearer thread-scoped-test-token",
+ browserToolsAvailable: true,
+ });
+ for (const nativeId of [undefined, "saved-native-session"]) {
+ const fake = yield* makeFakeMuse();
+ fake.host.initializeResult.grantedCapabilities.push("sessionMcp");
+ yield* makeHarness(fake, INSTANCE_ID, nativeId);
+ const call = yield* fake.takeCall(nativeId ? "session/resume" : "session/start");
+ assert.deepStrictEqual(call.params.config, {
+ mcpServers: {
+ "t3-code": {
+ transport: "streamableHttp",
+ mode: "optional",
+ url: "http://127.0.0.1:43210/mcp",
+ headers: { Authorization: "Bearer thread-scoped-test-token" },
+ },
+ },
+ });
+ }
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("rejects a host without session MCP support before opening a native session", () =>
+ Effect.gen(function* () {
+ yield* Effect.addFinalizer(() =>
+ Effect.sync(() => McpProviderSession.clearMcpProviderSession(THREAD_ID)),
+ );
+ McpProviderSession.setMcpProviderSession({
+ environmentId: EnvironmentId.make("test-environment"),
+ threadId: THREAD_ID,
+ providerSessionId: "test-session",
+ providerInstanceId: INSTANCE_ID,
+ endpoint: "http://127.0.0.1:43210/mcp",
+ authorizationHeader: "Bearer thread-scoped-test-token",
+ browserToolsAvailable: false,
+ });
+ const fake = yield* makeFakeMuse();
+ const outcome = yield* makeHarness(fake).pipe(Effect.result);
+ assert.strictEqual(outcome._tag, "Failure");
+ assert.isFalse(fake.calls.some((call) => call.method === "session/start"));
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect(
+ "starts fresh after recovery clears a native ref instead of reusing the startup hint",
+ () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const cleared = yield* preallocatedProviderThread();
+ const harness = yield* makeHarness(
+ fake,
+ INSTANCE_ID,
+ "failed-old-session",
+ undefined,
+ cleared,
+ );
+ const started = yield* fake.takeCall("session/start");
+ assert.isFalse(fake.calls.some((call) => call.method === "session/resume"));
+ assert.notStrictEqual(started.params.sessionId, "failed-old-session");
+ assert.strictEqual(harness.providerThread.id, cleared.id);
+ yield* startConversation(harness, fake);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("retries a missing native session with a cleared ref on the same healthy host", () =>
+ Effect.gen(function* () {
+ for (const kind of ["sessionNotFound", "notFound"] as const) {
+ const fake = yield* makeFakeMuse();
+ const allocated = yield* preallocatedProviderThread();
+ const adapter = makeMuseAdapterV2({
+ instanceId: INSTANCE_ID,
+ settings: museSettings,
+ environment: { PATH: "/fake/bin" },
+ idAllocator: yield* IdAllocator.IdAllocatorV2,
+ host: yield* ProviderHost,
+ fileSystem: yield* FileSystem.FileSystem,
+ createHost: async () => fake.host,
+ });
+ const runtime = yield* adapter.openSession({
+ threadId: THREAD_ID,
+ providerSessionId: ProviderSessionId.make(`missing-${kind}`),
+ modelSelection: modelSelection(),
+ runtimePolicy,
+ initialNativeThreadId: "missing-session",
+ });
+ fake.queueResponse(
+ "session/resume",
+ new MspError({ code: -32000, message: "Saved session is absent", data: { kind } }),
+ );
+ const failed = yield* runtime
+ .ensureThread({
+ threadId: THREAD_ID,
+ modelSelection: modelSelection(),
+ runtimePolicy,
+ existingProviderThread: {
+ ...allocated,
+ nativeThreadRef: {
+ driver: MUSE_PROVIDER,
+ nativeId: "missing-session",
+ strength: "strong",
+ },
+ },
+ })
+ .pipe(Effect.exit);
+ assert.strictEqual(failed._tag, "Failure");
+ assert.strictEqual(fake.closeCount(), 0);
+ const fresh = yield* runtime.ensureThread({
+ threadId: THREAD_ID,
+ modelSelection: modelSelection(),
+ runtimePolicy,
+ existingProviderThread: allocated,
+ });
+ assert.strictEqual(fresh.id, allocated.id);
+ assert.notStrictEqual(fresh.nativeThreadRef?.nativeId, "missing-session");
+ assert.strictEqual(fake.closeCount(), 0);
+ yield* runtime.startTurn(yield* turnInput(fresh));
+ const admitted = yield* fake.takeCall("turn/start");
+ assert.strictEqual(admitted.params.reasoningEffort, "max");
+ yield* fake.emit("turn/completed", { turnId: admitted.commandId, terminal: "completed" });
+ yield* Stream.runDrain(
+ runtime.events.pipe(Stream.takeUntil((event) => event.type === "turn.terminal")),
+ );
+ }
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("rejects cross-instance ensure requests without closing the current valid host", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const rejected = yield* harness.runtime
+ .ensureThread({
+ threadId: THREAD_ID,
+ modelSelection: modelSelection(),
+ runtimePolicy,
+ existingProviderThread: {
+ ...harness.providerThread,
+ providerInstanceId: ProviderInstanceId.make("other-account"),
+ },
+ })
+ .pipe(Effect.exit);
+ assert.strictEqual(rejected._tag, "Failure");
+ assert.strictEqual(fake.closeCount(), 0);
+ yield* startConversation(harness, fake);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("skips an unreadable usage notification without ending the turn", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("session/tokenUsage", { turnId: nativeId, usage: "not usage" });
+ yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "completed");
+ assert.strictEqual(fake.closeCount(), 0);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("moves a resumed session on another model back to the catalog default", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ // The saved session last ran MODEL; the catalog's default is another model.
+ const harness = yield* makeHarness(
+ fake,
+ INSTANCE_ID,
+ "saved-session",
+ undefined,
+ undefined,
+ runtimePolicy,
+ {
+ modelCatalog: Effect.succeed([
+ {
+ slug: "account-default",
+ name: "Default",
+ isCustom: false,
+ isDefault: true,
+ capabilities: null,
+ },
+ { slug: MODEL, name: "Other", isCustom: false, isDefault: false, capabilities: null },
+ ]),
+ },
+ );
+ const input = yield* turnInput(harness.providerThread, 1);
+ const selection = { ...input.modelSelection, model: MUSE_DEFAULT_MODEL };
+ yield* harness.runtime.startTurn({
+ ...input,
+ modelSelection: selection,
+ appThread: { ...input.appThread, modelSelection: selection },
+ });
+ const setModel = yield* fake.takeCall("session/setModel");
+ assert.deepStrictEqual(setModel.params.model, {
+ modelId: "account-default",
+ providerId: "meta",
+ });
+ yield* fake.takeCall("turn/start");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("settles partial messages and pending approvals before a provider failure", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("item/started", {
+ turnId: nativeId,
+ item: {
+ itemId: "partial",
+ turnId: nativeId,
+ kind: "agentMessage",
+ status: "inProgress",
+ revision: 1,
+ text: "Partial answer",
+ },
+ });
+ yield* fake.emit("approval/requested", approval(nativeId));
+ yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ yield* fake.emit("turn/completed", {
+ turnId: nativeId,
+ terminal: "failed",
+ error: { message: "Provider rejected this turn" },
+ });
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "failed");
+ assert.strictEqual(terminal.threadDisposition, "reusable");
+ assert.isNotNull(terminal.failure);
+ assert.isTrue(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "message.updated" &&
+ !event.message.streaming &&
+ event.message.text === "Partial answer",
+ ),
+ );
+ assert.isTrue(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "runtime_request.updated" && event.runtimeRequest.status === "cancelled",
+ ),
+ );
+ yield* startConversation(harness, fake, 2);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("closes its SDK host exactly once when its session scope ends", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ yield* makeHarness(fake).pipe(Effect.scoped);
+ assert.strictEqual(fake.closeCount(), 1);
+ }).pipe(Effect.provide(testLayer)),
+ );
+
+ it.effect(
+ "closes a newly opened host when native session registration returns another identity",
+ () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ fake.queueResponse("session/start", {
+ session: { sessionId: "unexpected-session", modelId: MODEL, activeTurnId: null },
+ history: { items: [] },
+ });
+ const result = yield* makeHarness(fake).pipe(Effect.exit);
+ assert.strictEqual(result._tag, "Failure");
+ // A failed host closes in the background so the failure shows at once.
+ yield* Effect.promise(() => fake.host.exited);
+ assert.strictEqual(fake.closeCount(), 1);
+ assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 0);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("settles an active turn once when both the connection and host exit unexpectedly", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("approval/requested", approval(nativeId));
+ yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ yield* fake.disconnect();
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "failed");
+ assert.strictEqual(terminal.threadDisposition, "broken");
+ assert.isTrue(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "runtime_request.updated" && event.runtimeRequest.status === "cancelled",
+ ),
+ );
+ const result = yield* harness.runtime
+ .startTurn(yield* turnInput(harness.providerThread, 2))
+ .pipe(Effect.exit);
+ assert.strictEqual(result._tag, "Failure");
+ assert.strictEqual(
+ harness.allEvents.filter((event) => event.type === "turn.terminal").length,
+ 1,
+ );
+ assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 1);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("turns a delivery gap into a broken terminal and rejects subsequent work", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ yield* startConversation(harness, fake);
+ yield* fake.emit("view/gap", {});
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "failed");
+ assert.strictEqual(terminal.threadDisposition, "broken");
+ yield* Effect.promise(() => fake.host.exited);
+ assert.strictEqual(fake.closeCount(), 1);
+ yield* Deferred.await(harness.eventsEnded);
+ const result = yield* harness.runtime
+ .startTurn(yield* turnInput(harness.providerThread, 2))
+ .pipe(Effect.exit);
+ assert.strictEqual(result._tag, "Failure");
+ assert.strictEqual(fake.calls.filter((call) => call.method === "turn/start").length, 1);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("uses the latest native approval requirement and waits for its resolution event", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("approval/requested", approval(nativeId));
+ const first = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ yield* fake.emit("approval/updated", {
+ ...approval(nativeId),
+ currentRequirementId: { approvalId: "approval-1", sourceIndex: 1 },
+ });
+ const refreshed = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ assert.strictEqual(refreshed.runtimeRequest.id, first.runtimeRequest.id);
+ yield* harness.runtime.respondToRuntimeRequest({
+ requestId: first.runtimeRequest.id,
+ decision: "accept",
+ });
+ const decision = yield* fake.takeCall("approval/decide");
+ assert.deepStrictEqual(decision.params.requirementId, {
+ approvalId: "approval-1",
+ sourceIndex: 1,
+ });
+ assert.strictEqual(decision.params.choiceId, "once");
+ assert.isFalse(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "runtime_request.updated" && event.runtimeRequest.status === "resolved",
+ ),
+ );
+ yield* fake.emit("approval/resolved", { approvalId: "approval-1", turnId: nativeId });
+ const resolved = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "resolved",
+ );
+ assert.strictEqual(resolved.runtimeRequest.id, first.runtimeRequest.id);
+ const repeated = yield* harness.runtime
+ .respondToRuntimeRequest({ requestId: first.runtimeRequest.id, decision: "accept" })
+ .pipe(Effect.exit);
+ assert.strictEqual(repeated._tag, "Failure");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("keeps the user's decision when Muse settles before acknowledging it", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("approval/requested", approval(nativeId));
+ const pending = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ const acknowledgement = pendingPromise>();
+ fake.queueResponse("approval/decide", acknowledgement.promise);
+ const responding = yield* harness.runtime
+ .respondToRuntimeRequest({ requestId: pending.runtimeRequest.id, decision: "decline" })
+ .pipe(Effect.forkScoped);
+ yield* fake.takeCall("approval/decide");
+ // Muse's native decision for this choice maps to decline too; "cancel" proves it was ours.
+ yield* fake.emit("approval/resolved", {
+ approvalId: "approval-1",
+ turnId: nativeId,
+ decision: "abort",
+ });
+ const resolved = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "resolved",
+ );
+ assert.strictEqual(resolved.runtimeRequest.decision, "decline");
+ acknowledgement.resolve({});
+ yield* Fiber.join(responding);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("lets a user turn take a held Muse report turn, approvals included", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const offers: Array = [];
+ const harness = yield* makeHarness(
+ fake,
+ INSTANCE_ID,
+ undefined,
+ undefined,
+ undefined,
+ runtimePolicy,
+ { continuationRequests: { offer: (request) => Effect.sync(() => offers.push(request)) } },
+ );
+ // A finished workflow's report turn starts while T3 runs nothing: it is held.
+ yield* fake.emit("turn/started", { turnId: "report-1" });
+ yield* Effect.yieldNow;
+ assert.lengthOf(offers, 1);
+ // The user sends a message first; Muse runs the report before it.
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("approval/requested", approval("report-1"));
+ const asked = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "pending",
+ );
+ yield* harness.runtime.respondToRuntimeRequest({
+ requestId: asked.runtimeRequest.id,
+ decision: "accept",
+ });
+ yield* fake.takeCall("approval/decide");
+ yield* fake.emit("approval/resolved", { approvalId: "approval-1", turnId: "report-1" });
+ yield* fake.emit("turn/completed", { turnId: "report-1", terminal: "completed" });
+ yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "completed");
+ // The user turn took it, so the continuation is not dispatched.
+ const dispatched = yield* offers[0]!.dispatchIfCurrent!(Effect.succeed("run"));
+ assert.isTrue(Option.isNone(dispatched));
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("approves a workflow child's approval in full access after its turn ended", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
+ yield* harness.takeEvent("turn.terminal");
+ // Muse runs workflow children without the session's allowAll mode.
+ const childApproval = { ...approval("child-run-1"), subagentOrigin: { subagentId: "a7" } };
+ yield* fake.emit("approval/requested", childApproval);
+ const asked = yield* harness.takeEvent("runtime_request.updated");
+ assert.strictEqual(asked.runtimeRequest.status, "pending");
+ assert.isNull(asked.runtimeRequest.providerTurnId);
+ const decision = yield* fake.takeCall("approval/decide");
+ assert.strictEqual(decision.params.choiceId, "once");
+ yield* fake.emit("approval/resolved", childApproval);
+ const resolved = yield* harness.takeEvent("runtime_request.updated");
+ assert.strictEqual(resolved.runtimeRequest.status, "resolved");
+ assert.strictEqual(resolved.runtimeRequest.decision, "accept");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("keeps a workflow child's approval pending for the user past the turn's end", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const policy = ProviderAdapterV2RuntimePolicy.make({
+ ...runtimePolicy,
+ runtimeMode: "approval-required",
+ });
+ const harness = yield* makeHarness(
+ fake,
+ INSTANCE_ID,
+ undefined,
+ undefined,
+ undefined,
+ policy,
+ );
+ const { nativeId } = yield* startConversation(harness, fake);
+ const childApproval = { ...approval("child-run-1"), subagentOrigin: { subagentId: "a7" } };
+ yield* fake.emit("approval/requested", childApproval);
+ const asked = yield* harness.takeEvent("runtime_request.updated");
+ assert.isNull(asked.runtimeRequest.providerTurnId);
+ yield* fake.emit("turn/completed", { turnId: nativeId, terminal: "completed" });
+ yield* harness.takeEvent("turn.terminal");
+ assert.isFalse(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "runtime_request.updated" && event.runtimeRequest.status !== "pending",
+ ),
+ );
+ yield* harness.runtime.respondToRuntimeRequest({
+ requestId: asked.runtimeRequest.id,
+ decision: "decline",
+ });
+ const decision = yield* fake.takeCall("approval/decide");
+ assert.strictEqual(decision.params.choiceId, "deny");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("validates structured answers and preserves selected labels with a custom note", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ yield* fake.emit("userInput/requested", {
+ userInputId: "question-1",
+ turnId: nativeId,
+ questions: [
+ {
+ id: "approach",
+ header: "Approach",
+ question: "Which approach?",
+ options: [{ label: "Small" }, { label: "Large" }],
+ selection: { mode: "single" },
+ },
+ ],
+ });
+ const pending = yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.kind === "user_input",
+ );
+ const invalid = yield* harness.runtime
+ .respondToRuntimeRequest({
+ requestId: pending.runtimeRequest.id,
+ answers: { approach: ["Small", "Large"] },
+ })
+ .pipe(Effect.exit);
+ assert.strictEqual(invalid._tag, "Failure");
+ assert.isFalse(fake.calls.some((call) => call.method === "userInput/answer"));
+ yield* harness.runtime.respondToRuntimeRequest({
+ requestId: pending.runtimeRequest.id,
+ answers: { approach: ["Small", "Keep tests focused"] },
+ });
+ const response = yield* fake.takeCall("userInput/answer");
+ assert.deepStrictEqual(response.params.answers, [
+ { questionId: "approach", selectedLabel: "Small", note: "Keep tests focused" },
+ ]);
+ yield* fake.emit("userInput/settled", {
+ userInputId: "question-1",
+ turnId: nativeId,
+ answers: response.params.answers,
+ });
+ yield* harness.takeEvent(
+ "runtime_request.updated",
+ (event) => event.runtimeRequest.status === "resolved",
+ );
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("steers only the active native turn and waits for confirmed interruption", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const turn = yield* startConversation(harness, fake);
+ const input = yield* turnInput(harness.providerThread);
+ const invalid = yield* harness.runtime.steerTurn!({
+ threadId: THREAD_ID,
+ runId: input.runId,
+ providerThread: harness.providerThread,
+ providerTurnId: ProviderTurnId.make("unrelated-turn"),
+ message: input.message,
+ }).pipe(Effect.exit);
+ assert.strictEqual(invalid._tag, "Failure");
+ assert.isFalse(fake.calls.some((call) => call.method === "turn/steer"));
+ yield* harness.runtime.steerTurn!({
+ threadId: THREAD_ID,
+ runId: input.runId,
+ providerThread: harness.providerThread,
+ providerTurnId: turn.providerTurn.id,
+ message: { ...input.message, text: "Focus on the parser" },
+ });
+ assert.strictEqual((yield* fake.takeCall("turn/steer")).params.expectedTurnId, turn.nativeId);
+ const interrupt = yield* harness.runtime
+ .interruptTurn({
+ providerThread: harness.providerThread,
+ providerTurnId: turn.providerTurn.id,
+ })
+ .pipe(Effect.forkScoped);
+ assert.strictEqual((yield* fake.takeCall("turn/interrupt")).params.turnId, turn.nativeId);
+ yield* fake.emit("turn/completed", { turnId: turn.nativeId, terminal: "cancelled" });
+ yield* Fiber.join(interrupt);
+ assert.strictEqual((yield* harness.takeEvent("turn.terminal")).status, "interrupted");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("resumes the requested native session and reads the same session snapshot", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const nativeItem: MuseItem = {
+ itemId: "prior-message",
+ turnId: "prior-turn",
+ kind: "agentMessage",
+ status: "completed",
+ revision: 1,
+ text: "Previous answer",
+ };
+ fake.setHistory([nativeItem]);
+ const harness = yield* makeHarness(fake, INSTANCE_ID, "saved-session");
+ const resumed = yield* fake.takeCall("session/resume");
+ assert.strictEqual(resumed.params.sessionId, "saved-session");
+ assert.strictEqual(resumed.params.excludeItems, true);
+ assert.isFalse(fake.calls.some((call) => call.method === "session/start"));
+ assert.strictEqual(harness.providerThread.nativeThreadRef?.nativeId, "saved-session");
+ const snapshot = yield* harness.runtime.readThreadSnapshot!({
+ providerThread: harness.providerThread,
+ });
+ assert.strictEqual(snapshot.providerThread.nativeThreadRef?.nativeId, "saved-session");
+ assert.deepStrictEqual(snapshot.providerPayload, [nativeItem]);
+ assert.strictEqual(snapshot.messages[0]?.text, "Previous answer");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("serializes interruption behind pending turn admission without losing the target", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const admission = pendingPromise>();
+ fake.queueResponse("turn/start", admission.promise);
+ const starting = yield* harness.runtime
+ .startTurn(yield* turnInput(harness.providerThread))
+ .pipe(Effect.forkScoped);
+ const call = yield* fake.takeCall("turn/start");
+ const running = yield* harness.takeEvent(
+ "provider_turn.updated",
+ (event) => event.providerTurn.status === "running",
+ );
+ const stopping = yield* harness.runtime
+ .interruptTurn({
+ providerThread: harness.providerThread,
+ providerTurnId: running.providerTurn.id,
+ })
+ .pipe(Effect.forkScoped);
+ admission.resolve({ turnId: call.commandId });
+ yield* Fiber.join(starting);
+ assert.strictEqual((yield* fake.takeCall("turn/interrupt")).params.turnId, call.commandId);
+ yield* fake.emit("turn/completed", { turnId: call.commandId, terminal: "cancelled" });
+ yield* Fiber.join(stopping);
+ assert.strictEqual((yield* harness.takeEvent("turn.terminal")).status, "interrupted");
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("reports a no-op compaction as a visible failure and permits another turn", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ fake.queueResponse("session/compact", { status: "noop", reason: "no_compactable_history" });
+ yield* harness.runtime.compactThread!(yield* turnInput(harness.providerThread));
+ const terminal = yield* harness.takeEvent("turn.terminal");
+ assert.strictEqual(terminal.status, "failed");
+ assert.strictEqual(terminal.threadDisposition, "reusable");
+ yield* startConversation(harness, fake, 2);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+
+ it.effect("projects native todos with stable identity and removes cancelled steps", () =>
+ Effect.gen(function* () {
+ const fake = yield* makeFakeMuse();
+ const harness = yield* makeHarness(fake);
+ const { nativeId } = yield* startConversation(harness, fake);
+ // Neither the tool call that wrote the list nor Muse's per-step reminder
+ // child may show as a generic tool row.
+ yield* fake.emit("item/completed", {
+ turnId: nativeId,
+ item: {
+ itemId: "todo-tool",
+ turnId: nativeId,
+ kind: "toolCall",
+ tool: "write_todos",
+ revision: 1,
+ status: "completed",
+ },
+ });
+ yield* fake.emit("item/started", {
+ turnId: nativeId,
+ item: {
+ itemId: "reminder",
+ turnId: nativeId,
+ kind: "reminderChild",
+ revision: 1,
+ status: "inProgress",
+ fallbackText: "Reminder child session",
+ },
+ });
+ yield* fake.emit("session/todoListChanged", {
+ items: [
+ { text: " Investigate ", status: "completed" },
+ { text: "Fix", status: "inProgress" },
+ { text: "Discarded", status: "cancelled" },
+ { text: " ", status: "pending" },
+ ],
+ });
+ const first = yield* harness.takeEvent(
+ "turn_item.updated",
+ (event) => event.turnItem.type === "todo_list",
+ );
+ assert.strictEqual(first.turnItem.type, "todo_list");
+ if (first.turnItem.type !== "todo_list") return;
+ assert.deepStrictEqual(
+ first.turnItem.steps.map(({ text, status }) => ({ text, status })),
+ [
+ { text: "Investigate", status: "completed" },
+ { text: "Fix", status: "running" },
+ ],
+ );
+ const planId = first.turnItem.planId;
+ yield* fake.emit("session/todoListChanged", {
+ items: [
+ { text: "Investigate", status: "completed" },
+ { text: "Fix", status: "completed" },
+ ],
+ });
+ const next = yield* harness.takeEvent(
+ "turn_item.updated",
+ (event) => event.turnItem.type === "todo_list",
+ );
+ assert.strictEqual(next.turnItem.id, first.turnItem.id);
+ assert.strictEqual(next.turnItem.nodeId, first.turnItem.nodeId);
+ if (next.turnItem.type !== "todo_list") return;
+ assert.strictEqual(next.turnItem.planId, first.turnItem.planId);
+ assert.isTrue(
+ harness.allEvents.some(
+ (event) =>
+ event.type === "plan.updated" &&
+ event.plan.id === planId &&
+ event.plan.status === "completed",
+ ),
+ );
+ assert.isFalse(
+ harness.allEvents.some(
+ (event) => event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool",
+ ),
+ );
+ assert.isTrue((yield* harness.adapter.getCapabilities()).planning.emitsPlanUpdated);
+ assert.isTrue((yield* harness.adapter.getCapabilities()).planning.emitsTodoList);
+ }).pipe(Effect.scoped, Effect.provide(testLayer)),
+ );
+});
diff --git a/packages/provider-muse/src/server/adapter.ts b/packages/provider-muse/src/server/adapter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf1699055534c0b08ecd0399b33d79338abc190f
--- /dev/null
+++ b/packages/provider-muse/src/server/adapter.ts
@@ -0,0 +1,2047 @@
+import { MspError, type SendUserTurnOptions } from "@muse-code/sdk";
+import {
+ MUSE_DEFAULT_MODEL,
+ ProviderDriverKind,
+ type ModelSelection,
+ type OrchestrationV2ConversationMessage,
+ type OrchestrationV2ExecutionNode,
+ type OrchestrationV2ProviderCapabilities,
+ type OrchestrationV2ProviderSession,
+ type OrchestrationV2ProviderThread,
+ type OrchestrationV2ProviderTurn,
+ type OrchestrationV2RuntimeRequest,
+ type OrchestrationV2Subagent,
+ type OrchestrationV2TurnItemStatus,
+ type NodeId,
+ type OrchestrationV2TurnItem,
+ type ProviderInstanceId,
+ type PlanId,
+ type OrchestrationV2PlanStep,
+ type ProviderApprovalDecision,
+ type RuntimeRequestId,
+ type ServerProviderModel,
+} from "@t3tools/contracts";
+import type { MuseSettings } from "../settings.ts";
+import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
+import * as Cause from "effect/Cause";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import * as FileSystem from "effect/FileSystem";
+import * as Queue from "effect/Queue";
+import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
+import * as Scope from "effect/Scope";
+import * as Stream from "effect/Stream";
+
+import type { ProviderHostShape } from "@t3tools/provider-core/server/ProviderHost";
+import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
+import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
+import { museModelCapabilities, resolveMuseReasoningEffort } from "./modelCatalog.ts";
+import {
+ MuseApproval,
+ MuseCompactResult,
+ MuseContextUsage,
+ MuseDelta,
+ MuseItemEvent,
+ MuseSessionResult,
+ MuseTokenUsageEvent,
+ MuseTodoList,
+ MuseTurnRetryScheduled,
+ MuseTurnCompleted,
+ MuseTurnStartResult,
+ MuseUserInput,
+ museApprovalChoices,
+ museApprovalDecision,
+ museApprovalOptions,
+ type MuseItem,
+} from "./protocol.ts";
+import {
+ createMuseSdkHostEffect,
+ museApprovalMode,
+ type createMuseSdkHost,
+ type MuseSdkHost,
+} from "./sdk.ts";
+import type { EventNdjsonLogger } from "@t3tools/provider-core/server/ProviderEventLoggers";
+import {
+ providerMessageTextWithAttachmentPaths,
+ isProviderNativeImageAttachment,
+} from "@t3tools/provider-core/server/attachmentPrompt";
+import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
+import {
+ ProviderAdapterEnsureThreadError,
+ ProviderAdapterForkThreadError,
+ ProviderAdapterInterruptError,
+ ProviderAdapterOpenSessionError,
+ ProviderAdapterProtocolError,
+ ProviderAdapterReadThreadSnapshotError,
+ ProviderAdapterResumeThreadError,
+ ProviderAdapterRollbackThreadError,
+ ProviderAdapterRuntimeRequestResponseError,
+ ProviderAdapterSteerRunError,
+ ProviderAdapterTurnStartError,
+ ProviderAdapterV2,
+ type ProviderAdapterV2Error,
+ type ProviderAdapterV2Event,
+ type ProviderAdapterV2EnsureThreadInput,
+ type ProviderAdapterV2OpenSessionInput,
+ type ProviderAdapterV2SessionRuntime,
+ type ProviderAdapterV2Shape,
+ type ProviderAdapterV2ThreadSnapshot,
+ type ProviderAdapterV2TurnInput,
+ type ProviderAdapterV2TurnMessage,
+} from "@t3tools/provider-core/server/ProviderAdapter";
+import {
+ backgroundWorkNotification,
+ type BackgroundWorkReport,
+} from "@t3tools/provider-core/server/notification";
+import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
+import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
+import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
+import { museItemStatus, museToolPresentation } from "./itemPresentation.ts";
+
+const MUSE_PROVIDER = ProviderDriverKind.make("muse");
+const isOpenSessionError = Schema.is(ProviderAdapterOpenSessionError);
+const isProtocolError = Schema.is(ProviderAdapterProtocolError);
+
+const MuseProviderCapabilitiesV2 = {
+ runtimePolicy: { enforcement: "native" },
+ sessions: {
+ supportsMultipleProviderThreadsPerSession: false,
+ supportsModelSwitchInSession: true,
+ supportsProviderSwitchingViaHandoff: true,
+ supportsRuntimeModeSwitchInSession: false,
+ pendingRequestsSurviveRestart: false,
+ },
+ threads: {
+ canCreateEmptyThread: true,
+ canReadThreadSnapshot: true,
+ // Muse 1.2.1/1.3.0 reject tested completed-turn fork boundaries with InvalidCut.
+ // Public forks use portable context handoff until native boundaries are validated.
+ canRollbackThread: false,
+ canForkThread: false,
+ canForkFromTurn: false,
+ canForkFromSubagentThread: false,
+ exposesNativeThreadId: true,
+ },
+ turns: {
+ exposesNativeTurnId: true,
+ emitsTurnStarted: true,
+ emitsTurnCompleted: true,
+ supportsInterrupt: true,
+ supportsActiveSteering: true,
+ supportsSteeringByInterruptRestart: false,
+ supportsQueuedMessages: true,
+ terminalStatusQuality: "strong",
+ },
+ streaming: {
+ streamsAssistantText: true,
+ streamsReasoning: true,
+ streamsToolOutput: true,
+ streamsPlanText: false,
+ emitsMessageCompleted: true,
+ },
+ tools: {
+ exposesToolItemIds: true,
+ emitsToolStarted: true,
+ emitsToolCompleted: true,
+ emitsToolOutput: true,
+ supportsMcpTools: true,
+ supportsDynamicToolCallbacks: false,
+ },
+ approvals: {
+ supportsCommandApproval: true,
+ supportsFileReadApproval: true,
+ supportsFileChangeApproval: true,
+ supportsApplyPatchApproval: false,
+ approvalsHaveNativeRequestIds: true,
+ approvalCallbacksAreLiveOnly: true,
+ approvalsCanOriginateFromSubagents: true,
+ },
+ planning: {
+ emitsPlanUpdated: true,
+ emitsTodoList: true,
+ emitsProposedPlan: false,
+ supportsStructuredQuestions: true,
+ planDeltasHaveItemIds: false,
+ },
+ subagents: {
+ supportsSubagents: false,
+ exposesSubagentThreadIds: false,
+ emitsSubagentLifecycle: false,
+ canWaitForSubagents: false,
+ canCloseSubagents: false,
+ canForkSubagentThread: false,
+ },
+ context: {
+ acceptsSystemContext: false,
+ acceptsDeveloperContext: false,
+ acceptsSyntheticUserContext: true,
+ canGenerateSummaries: false,
+ canConsumeHandoffSummaries: true,
+ supportsDeltaHandoff: true,
+ supportsFullThreadHandoff: true,
+ maxRecommendedHandoffChars: null,
+ },
+ checkpointing: {
+ appCanCheckpointFilesystem: true,
+ supportsNestedCheckpointScopes: false,
+ providerCanRollbackConversation: false,
+ providerRollbackReturnsSnapshot: false,
+ providerCanReadConversationSnapshot: true,
+ },
+ identity: {
+ nativeThreadIds: "strong",
+ nativeTurnIds: "strong",
+ nativeItemIds: "strong",
+ nativeRequestIds: "strong",
+ },
+} satisfies OrchestrationV2ProviderCapabilities;
+
+export interface MuseAdapterV2Options {
+ readonly instanceId: ProviderInstanceId;
+ readonly settings: MuseSettings;
+ readonly environment: NodeJS.ProcessEnv;
+ readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
+ readonly host: ProviderHostShape;
+ readonly fileSystem: FileSystem.FileSystem;
+ readonly modelCatalog?: Effect.Effect>;
+ readonly createHost?: typeof createMuseSdkHost;
+ readonly requestTimeoutMs?: number;
+ readonly nativeEventLogger?: EventNdjsonLogger;
+ /** Where a turn Muse starts on its own (a finished workflow's report) asks for its run. */
+ readonly continuationRequests?: {
+ readonly offer: (
+ request: ProviderContinuationRequests.ProviderContinuationRequest,
+ ) => Effect.Effect;
+ };
+}
+
+interface ActiveTurn {
+ readonly input: ProviderAdapterV2TurnInput;
+ providerTurn: OrchestrationV2ProviderTurn;
+ readonly nativeId: string;
+ /** Turns Muse started on its own while this run was active, which this run shows. */
+ readonly joined: Set;
+ readonly items: Map;
+ readonly ordinals: Map;
+ readonly started: Map;
+ readonly dirty: Set;
+ readonly settledRequests: Set;
+ readonly done: Deferred.Deferred;
+ todoPlanId?: PlanId;
+ nextOrdinal: number;
+ flushScheduled: boolean;
+ interruptRequested: boolean;
+ compact: boolean;
+}
+
+interface PendingRequest {
+ readonly native:
+ | { type: "approval"; value: MuseApproval }
+ | { type: "question"; value: MuseUserInput };
+ request: OrchestrationV2RuntimeRequest;
+ node: OrchestrationV2ExecutionNode;
+ item: OrchestrationV2TurnItem;
+ /** What T3 answered, recorded on the request once Muse confirms it. */
+ response?: Pick;
+}
+
+const nativeRef = (nativeId: string) => ({
+ driver: MUSE_PROVIDER,
+ nativeId,
+ strength: "strong" as const,
+});
+const recordSchema = Schema.Record(Schema.String, Schema.Unknown);
+// Notifications that only add detail. A malformed one is skipped instead of ending the session.
+const INFORMATIONAL_NOTIFICATIONS = new Set([
+ "session/contextUsage",
+ "session/tokenUsage",
+ "session/todoListChanged",
+ "turn/retryScheduled",
+]);
+// Tools whose results already show as their own rows: T3's todo list and question
+// rows, and the workflow item a `workflow` call launches.
+const TOOLS_WITH_NATIVE_ROWS = new Set(["write_todos", "request_user_input", "workflow"]);
+const responseAnswerSchema = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
+
+/** One scoped Muse host owns one native session; the orchestrator owns app runs and queuing. */
+export function makeMuseAdapterV2(options: MuseAdapterV2Options): ProviderAdapterV2Shape {
+ const { idAllocator } = options;
+ const protocolError = (detail: string, payload?: unknown) =>
+ new ProviderAdapterProtocolError({
+ driver: MUSE_PROVIDER,
+ detail,
+ ...(payload === undefined ? {} : { payload }),
+ });
+ return ProviderAdapterV2.of({
+ instanceId: options.instanceId,
+ driver: MUSE_PROVIDER,
+ getCapabilities: () => Effect.succeed(MuseProviderCapabilitiesV2),
+ planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
+ openSession: Effect.fn("MuseAdapterV2.openSession")(function* (
+ input: ProviderAdapterV2OpenSessionInput,
+ ) {
+ if (!options.settings.enabled) return yield* protocolError("Muse Code is disabled");
+ if (input.runtimePolicy.interactionMode === "plan")
+ return yield* protocolError("Muse Code does not support dedicated Plan mode");
+ const scope = yield* Effect.scope;
+ // Muse rejects non-canonical workspace roots (for example macOS /tmp) and
+ // reports canonical paths in approvals, so resolve symlinks once here.
+ const requestedCwd = input.runtimePolicy.cwd ?? options.host.paths.cwd;
+ const cwd = yield* options.fileSystem
+ .realPath(requestedCwd)
+ .pipe(Effect.orElseSucceed(() => requestedCwd));
+ const now = yield* DateTime.now;
+ let session: OrchestrationV2ProviderSession = {
+ id: input.providerSessionId,
+ driver: MUSE_PROVIDER,
+ providerInstanceId: options.instanceId,
+ status: "ready",
+ cwd,
+ model: input.modelSelection.model,
+ capabilities: MuseProviderCapabilitiesV2,
+ createdAt: now,
+ updatedAt: now,
+ lastError: null,
+ };
+ const events = yield* Queue.unbounded<
+ ProviderAdapterV2Event,
+ ProviderAdapterV2Error | Cause.Done
+ >();
+ type Inbox =
+ | { type: "notification"; method: string; params: unknown; epoch: number }
+ | { type: "failure"; cause: unknown; epoch: number }
+ | { type: "flush"; turn: ActiveTurn };
+ const inbox = yield* Queue.unbounded();
+ const commands = yield* Semaphore.make(1);
+ const eventPermit = yield* Semaphore.make(1);
+ let host: MuseSdkHost;
+ let hostEpoch = 0;
+ let closed = false;
+ let broken = false;
+ let nativeSessionId: string | undefined;
+ let thread: OrchestrationV2ProviderThread | undefined;
+ let active: ActiveTurn | undefined;
+ // The last finished turn, so context usage reported after it still lands on it.
+ let lastProviderTurn: OrchestrationV2ProviderTurn | undefined;
+ const agentEndedAt = new Map();
+ // "default" is the catalog's default model, from the provider snapshot. Without
+ // one, Muse keeps whatever model the session already runs.
+ const resolveModel = Effect.fnUntraced(function* (selection: ModelSelection) {
+ if (selection.model !== MUSE_DEFAULT_MODEL) return selection.model;
+ const catalog = options.modelCatalog ? yield* options.modelCatalog : [];
+ return catalog.find((model) => model.isDefault && !model.isCustom)?.slug;
+ });
+ const pending = new Map();
+ const seenTerminals = new Set();
+ // Workflows and subagents that outlive their turn, by item id, with the turn that owns them.
+ const observedChildren = new Map();
+ // Background work that finished since the last turn, reported to the turn Muse starts for it.
+ const finishedBackground: Array = [];
+ // A turn Muse started on its own, held until the continuation run T3 opens for it takes it.
+ let wake:
+ | { readonly nativeId: string; readonly events: Array<[string, unknown]> }
+ | undefined;
+ const emit = (event: ProviderAdapterV2Event) =>
+ Queue.offer(events, event).pipe(Effect.asVoid);
+ const decode = (schema: Schema.Codec, data: unknown) =>
+ Schema.decodeUnknownEffect(schema)(data).pipe(
+ Effect.mapError((cause) => protocolError("Invalid Muse protocol response", cause)),
+ );
+ const request = (
+ method: string,
+ params: Record,
+ command = true,
+ commandId?: string,
+ ) =>
+ Effect.tryPromise({
+ try: () =>
+ command
+ ? host.connection.command(
+ method,
+ { ...(nativeSessionId ? { sessionId: nativeSessionId } : {}), ...params },
+ {
+ ...(method === "turn/start" ? {} : { maxAttempts: 1 }),
+ ...(commandId ? { commandId } : {}),
+ },
+ )
+ : host.connection.request(method, {
+ ...(nativeSessionId ? { sessionId: nativeSessionId } : {}),
+ ...params,
+ }),
+ catch: (cause) =>
+ protocolError(
+ cause instanceof Error && cause.message
+ ? `Muse ${method} failed: ${cause.message}`
+ : `Muse ${method} failed`,
+ cause,
+ ),
+ }).pipe(
+ Effect.timeout(options.requestTimeoutMs ?? 30_000),
+ Effect.catchTags({
+ TimeoutError: (cause) =>
+ Effect.gen(function* () {
+ yield* eventPermit.withPermits(1)(failHost(cause));
+ return yield* protocolError(`Muse ${method} timed out; its host was closed`, cause);
+ }),
+ }),
+ );
+ const updateSession = Effect.fnUntraced(function* (
+ status: OrchestrationV2ProviderSession["status"],
+ lastError: string | null = null,
+ ) {
+ session = { ...session, status, lastError, updatedAt: yield* DateTime.now };
+ yield* emit({
+ type: "provider_session.updated",
+ driver: MUSE_PROVIDER,
+ providerSession: session,
+ });
+ });
+ const updateThread = Effect.fnUntraced(function* (
+ patch: Partial,
+ ) {
+ if (!thread) return;
+ thread = { ...thread, ...patch, updatedAt: yield* DateTime.now };
+ yield* emit({
+ type: "provider_thread.updated",
+ driver: MUSE_PROVIDER,
+ providerThread: thread,
+ });
+ });
+ const itemIdentity = (turn: ActiveTurn, nativeId: string) =>
+ `${options.instanceId}:${turn.input.providerThread.id}:${turn.nativeId}:${nativeId}`;
+ const baseItem = (turn: ActiveTurn, nativeId: string, time: DateTime.Utc) => {
+ if (!turn.ordinals.has(nativeId)) turn.ordinals.set(nativeId, turn.nextOrdinal++);
+ if (!turn.started.has(nativeId)) turn.started.set(nativeId, time);
+ const identity = itemIdentity(turn, nativeId);
+ return {
+ id: idAllocator.derive.turnItemFromProviderItem({
+ driver: MUSE_PROVIDER,
+ nativeItemId: identity,
+ }),
+ nodeId: idAllocator.derive.nodeFromProviderItem({
+ driver: MUSE_PROVIDER,
+ nativeItemId: identity,
+ }),
+ threadId: turn.input.threadId,
+ runId: turn.input.runId,
+ providerThreadId: turn.input.providerThread.id,
+ providerTurnId: turn.providerTurn.id,
+ nativeItemRef: nativeRef(nativeId),
+ parentItemId: null,
+ ordinal: turn.ordinals.get(nativeId)!,
+ startedAt: turn.started.get(nativeId)!,
+ updatedAt: time,
+ };
+ };
+ /** Lists running workflows and subagents on the thread, so it shows it still waits on them. */
+ const syncBackground = Effect.fnUntraced(function* () {
+ yield* updateThread({
+ pendingBackgroundTasks: [...observedChildren].flatMap(([itemId, owner]) => {
+ const item = owner.items.get(itemId);
+ const description = item?.objective ?? item?.entryId ?? item?.fallbackText;
+ return [
+ {
+ taskId: itemId,
+ kind:
+ item?.kind === "subagent" ? ("subagent" as const) : ("background_task" as const),
+ ...(description ? { description } : {}),
+ },
+ ];
+ }),
+ });
+ });
+ const publishItem = Effect.fnUntraced(function* (
+ turn: ActiveTurn,
+ item: MuseItem,
+ terminal?: OrchestrationV2TurnItem["status"],
+ ) {
+ // Muse's skill-reminder child runs on every model step; it is housekeeping, not work.
+ if (item.kind === "userMessage" || item.kind === "reminderChild") return;
+ // A failed call keeps its row: it may have no native row to show its error.
+ if (
+ item.kind === "toolCall" &&
+ item.tool &&
+ TOOLS_WITH_NATIVE_ROWS.has(item.tool) &&
+ item.status !== "failed"
+ )
+ return;
+ const time = yield* DateTime.now;
+ const status = terminal ?? museItemStatus(item);
+ if (item.kind === "subagent" || item.kind === "workflow") {
+ const wasRunning = observedChildren.has(item.itemId);
+ if (status === "running") observedChildren.set(item.itemId, turn);
+ else if (observedChildren.delete(item.itemId) && active !== turn)
+ finishedBackground.push({
+ kind: item.kind === "subagent" ? "subagent" : "background_task",
+ label: item.objective ?? item.entryId ?? item.fallbackText,
+ outcome:
+ status === "completed"
+ ? "completed"
+ : status === "cancelled"
+ ? "cancelled"
+ : "failed",
+ });
+ if (wasRunning !== observedChildren.has(item.itemId)) yield* syncBackground();
+ }
+ const streaming = status === "running";
+ const base = {
+ ...baseItem(turn, item.itemId, time),
+ status,
+ completedAt: streaming ? null : time,
+ title: item.tool ?? null,
+ };
+ const text = item.text ?? item.summary?.join("\n") ?? item.fallbackText ?? "";
+ let turnItem: OrchestrationV2TurnItem;
+ if (item.kind === "agentMessage") {
+ const messageId = idAllocator.derive.messageFromProviderItem({
+ driver: MUSE_PROVIDER,
+ nativeItemId: itemIdentity(turn, item.itemId),
+ });
+ const message: OrchestrationV2ConversationMessage = {
+ id: messageId,
+ threadId: turn.input.threadId,
+ runId: turn.input.runId,
+ nodeId: base.nodeId,
+ role: "assistant",
+ text,
+ attachments: [],
+ streaming,
+ createdBy: "agent",
+ creationSource: "provider",
+ createdAt: base.startedAt,
+ updatedAt: time,
+ };
+ yield* emit({ type: "message.updated", driver: MUSE_PROVIDER, message });
+ turnItem = { ...base, type: "assistant_message", messageId, text, streaming };
+ } else if (item.kind === "reasoning") {
+ turnItem = { ...base, type: "reasoning", text, streaming };
+ } else if (item.kind === "compaction") {
+ turnItem = {
+ ...base,
+ type: "compaction",
+ driver: MUSE_PROVIDER,
+ summary: text || item.reason,
+ };
+ } else {
+ turnItem = { ...base, ...museToolPresentation(item, status) };
+ }
+ yield* emit({
+ type: "node.updated",
+ driver: MUSE_PROVIDER,
+ node: {
+ id: base.nodeId,
+ threadId: base.threadId,
+ runId: base.runId,
+ parentNodeId: turn.input.rootNodeId,
+ rootNodeId: turn.input.rootNodeId,
+ kind:
+ item.kind === "agentMessage"
+ ? "assistant_message"
+ : item.kind === "reasoning"
+ ? "reasoning"
+ : "tool_call",
+ status,
+ countsForRun: false,
+ providerThreadId: base.providerThreadId,
+ providerTurnId: base.providerTurnId,
+ nativeItemRef: base.nativeItemRef,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: base.startedAt,
+ completedAt: base.completedAt,
+ },
+ });
+ yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem });
+ if (item.kind === "workflow") yield* publishWorkflowAgents(turn, item, base, status);
+ });
+ /**
+ * Shows each workflow child as a native subagent under the workflow row. Muse
+ * reports only their status, label and outcome, not their conversations, so
+ * they get no child thread.
+ */
+ const publishWorkflowAgents = Effect.fnUntraced(function* (
+ turn: ActiveTurn,
+ item: MuseItem,
+ workflow: { readonly id: OrchestrationV2TurnItem["id"]; readonly nodeId: NodeId },
+ workflowStatus: OrchestrationV2TurnItemStatus,
+ ) {
+ const latest = new Map((item.children ?? []).map((child) => [child.childId, child]));
+ for (const [index, child] of [...latest.values()].entries()) {
+ const time = yield* DateTime.now;
+ const base = baseItem(turn, `${item.itemId}:agent:${child.childId}`, time);
+ const status: OrchestrationV2TurnItemStatus =
+ child.terminal === "completed"
+ ? "completed"
+ : child.terminal === "cancelled"
+ ? "cancelled"
+ : child.terminal !== undefined
+ ? "failed"
+ : workflowStatus !== "running"
+ ? workflowStatus
+ : child.status === "scheduled"
+ ? "pending"
+ : "running";
+ const settled = status !== "running" && status !== "pending";
+ // Children are re-sent on every workflow change; keep each one's first end time.
+ const endKey = `${item.itemId}:${child.childId}:${child.attempt}`;
+ if (settled && !agentEndedAt.has(endKey)) agentEndedAt.set(endKey, time);
+ const title = child.label?.trim() || `Agent ${index + 1}`;
+ const result = child.failureReason ?? null;
+ const subagent: OrchestrationV2Subagent = {
+ id: base.nodeId,
+ threadId: base.threadId,
+ runId: base.runId,
+ parentNodeId: workflow.nodeId,
+ origin: "provider_native",
+ createdBy: "agent",
+ driver: MUSE_PROVIDER,
+ providerInstanceId: options.instanceId,
+ providerThreadId: base.providerThreadId,
+ childThreadId: null,
+ nativeTaskRef: nativeRef(child.childId),
+ prompt: title,
+ title,
+ model: null,
+ status,
+ result,
+ startedAt: base.startedAt,
+ completedAt: settled ? (agentEndedAt.get(endKey) ?? time) : null,
+ updatedAt: time,
+ };
+ yield* emit({ type: "subagent.updated", driver: MUSE_PROVIDER, subagent });
+ yield* emit({
+ type: "node.updated",
+ driver: MUSE_PROVIDER,
+ node: {
+ id: base.nodeId,
+ threadId: base.threadId,
+ runId: base.runId,
+ parentNodeId: workflow.nodeId,
+ rootNodeId: turn.input.rootNodeId,
+ kind: "subagent",
+ status,
+ countsForRun: false,
+ providerThreadId: base.providerThreadId,
+ providerTurnId: base.providerTurnId,
+ nativeItemRef: subagent.nativeTaskRef,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: base.startedAt,
+ completedAt: subagent.completedAt,
+ },
+ });
+ yield* emit({
+ type: "turn_item.updated",
+ driver: MUSE_PROVIDER,
+ turnItem: {
+ ...base,
+ nativeItemRef: subagent.nativeTaskRef,
+ parentItemId: workflow.id,
+ type: "subagent",
+ status,
+ title,
+ completedAt: subagent.completedAt,
+ subagentId: subagent.id,
+ origin: "provider_native",
+ driver: MUSE_PROVIDER,
+ providerInstanceId: options.instanceId,
+ childThreadId: null,
+ prompt: title,
+ result,
+ },
+ });
+ }
+ });
+ const settleObservedChildren = Effect.fnUntraced(function* (
+ status: "failed" | "cancelled" | "interrupted",
+ ) {
+ for (const [itemId, owner] of observedChildren) {
+ const item = owner.items.get(itemId);
+ if (item) yield* publishItem(owner, item, status);
+ }
+ observedChildren.clear();
+ if (thread?.pendingBackgroundTasks?.length) yield* syncBackground();
+ // Only workflow children's requests outlive a turn; they end with the workflow.
+ for (const entry of pending.values()) yield* resolvePending(entry, "cancelled");
+ });
+ const resolvePending = Effect.fnUntraced(function* (
+ entry: PendingRequest,
+ status: "resolved" | "cancelled",
+ ) {
+ const time = yield* DateTime.now;
+ entry.request = {
+ ...entry.request,
+ ...(status === "resolved" ? entry.response : undefined),
+ status,
+ resolvedAt: time,
+ responseCapability: { type: "not_resumable", reason: "This Muse request has ended." },
+ };
+ entry.node = {
+ ...entry.node,
+ status: status === "resolved" ? "completed" : "cancelled",
+ completedAt: time,
+ };
+ entry.item = {
+ ...entry.item,
+ status: status === "resolved" ? "completed" : "cancelled",
+ completedAt: time,
+ updatedAt: time,
+ };
+ pending.delete(entry.request.id);
+ yield* emit({
+ type: "runtime_request.updated",
+ driver: MUSE_PROVIDER,
+ threadId: input.threadId,
+ runtimeRequest: entry.request,
+ });
+ yield* emit({ type: "node.updated", driver: MUSE_PROVIDER, node: entry.node });
+ yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem: entry.item });
+ });
+ const finish = Effect.fnUntraced(function* (
+ turn: ActiveTurn,
+ status: "completed" | "cancelled" | "interrupted" | "failed",
+ detail?: string,
+ disposition: "reusable" | "broken" = "reusable",
+ ) {
+ if (active !== turn || seenTerminals.has(turn.nativeId)) return;
+ seenTerminals.add(turn.nativeId);
+ const completedAt = yield* DateTime.now;
+ for (const item of turn.items.values()) {
+ if (
+ (item.kind === "subagent" || item.kind === "workflow") &&
+ item.status === "inProgress" &&
+ disposition === "reusable"
+ )
+ continue;
+ if (turn.dirty.has(item.itemId) || item.status === "inProgress")
+ yield* publishItem(turn, item, item.status === "inProgress" ? status : undefined);
+ }
+ turn.dirty.clear();
+ // A workflow child's request is runless and keeps waiting after the turn ends.
+ for (const entry of pending.values())
+ if (entry.request.providerTurnId !== null) yield* resolvePending(entry, "cancelled");
+ turn.providerTurn = { ...turn.providerTurn, status, completedAt };
+ lastProviderTurn = turn.providerTurn;
+ yield* emit({
+ type: "provider_turn.updated",
+ driver: MUSE_PROVIDER,
+ threadId: turn.input.threadId,
+ providerTurn: turn.providerTurn,
+ });
+ yield* updateThread({
+ status: disposition === "broken" ? "error" : "idle",
+ ...(!turn.compact && status === "completed"
+ ? { nativeConversationHeadRef: nativeRef(turn.nativeId) }
+ : {}),
+ });
+ yield* updateSession(disposition === "broken" ? "error" : "ready", detail ?? null);
+ active = undefined;
+ if (status === "failed") {
+ const failure = makeProviderFailure({
+ class: disposition === "broken" ? "transport_error" : "provider_error",
+ message: detail ?? "Muse turn failed.",
+ });
+ const base = baseItem(turn, `failure:${turn.nativeId}`, completedAt);
+ yield* emit({
+ type: "turn_item.updated",
+ driver: MUSE_PROVIDER,
+ turnItem: {
+ ...base,
+ type: "error",
+ status: "failed",
+ title: null,
+ completedAt,
+ failure,
+ },
+ });
+ yield* emit({
+ type: "turn.terminal",
+ driver: MUSE_PROVIDER,
+ providerThreadId: turn.providerTurn.providerThreadId,
+ providerTurnId: turn.providerTurn.id,
+ runOrdinal: turn.input.runOrdinal,
+ status,
+ failure,
+ failureItemOrdinal: base.ordinal,
+ threadDisposition: disposition,
+ });
+ } else {
+ yield* emit({
+ type: "turn.terminal",
+ driver: MUSE_PROVIDER,
+ providerThreadId: turn.providerTurn.providerThreadId,
+ providerTurnId: turn.providerTurn.id,
+ runOrdinal: turn.input.runOrdinal,
+ status,
+ failure: null,
+ threadDisposition: disposition,
+ });
+ }
+ yield* Deferred.succeed(turn.done, undefined);
+ });
+ const failHost = Effect.fnUntraced(function* (cause: unknown) {
+ if (closed || broken) return;
+ broken = true;
+ const rootCause = Cause.isCause(cause) ? Cause.squash(cause) : cause;
+ const failure = isOpenSessionError(rootCause) ? rootCause.cause : rootCause;
+ const detail = isProtocolError(failure)
+ ? failure.detail
+ : failure instanceof Error
+ ? failure.message
+ : "Muse transport failed.";
+ if (active) yield* finish(active, "failed", detail, "broken");
+ else {
+ yield* updateThread({ status: "error" });
+ yield* updateSession("error", detail);
+ }
+ yield* settleObservedChildren("failed");
+ yield* Queue.fail(events, protocolError(detail, cause));
+ // Report the failure first; a hung host can take a while to close.
+ const failed = host;
+ yield* Effect.tryPromise(() => failed.close()).pipe(Effect.ignore, Effect.forkIn(scope));
+ });
+ const owns = (turn: ActiveTurn, turnId: string) =>
+ turnId === turn.nativeId || turn.joined.has(turnId);
+ /** Sends T3's decision for a pending approval to Muse. */
+ const decideApproval = Effect.fnUntraced(function* (
+ entry: PendingRequest,
+ decision: ProviderApprovalDecision | undefined,
+ ) {
+ if (entry.native.type !== "approval") return;
+ const approval = entry.native.value;
+ const choice = decision && museApprovalChoices(approval).get(decision);
+ if (!choice) return yield* protocolError("Muse did not offer this approval decision");
+ // Recorded first: Muse can settle the approval before it acknowledges the command.
+ entry.response = { decision };
+ yield* request("approval/decide", {
+ approvalId: approval.approvalId,
+ requirementId: approval.currentRequirementId,
+ choiceId: choice.choiceId,
+ }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response)));
+ });
+ /**
+ * Shows a Muse approval or question as a T3 request. A workflow child's approval
+ * is runless: the turn that started the workflow may end long before the child
+ * asks, and T3 keeps a runless request on the thread until Muse settles it.
+ */
+ const publishRequest = Effect.fnUntraced(function* (native: PendingRequest["native"]) {
+ const child = native.type === "approval" && native.value.subagentOrigin !== undefined;
+ const turn = child ? undefined : active;
+ if (
+ !thread ||
+ (!child && !turn) ||
+ native.value.sessionId !== nativeSessionId ||
+ (turn && native.value.turnId && !owns(turn, native.value.turnId))
+ )
+ return;
+ const nativeId =
+ native.type === "approval" ? native.value.approvalId : native.value.userInputId;
+ if (turn?.settledRequests.has(`${native.type}:${nativeId}`)) return;
+ const previous = [...pending.values()].find(
+ (entry) =>
+ entry.native.type === native.type &&
+ entry.request.nativeRequestRef?.nativeId === nativeId,
+ );
+ const providerTurnId = turn?.providerTurn.id ?? null;
+ const threadId = turn?.input.threadId ?? thread.appThreadId ?? input.threadId;
+ const requestId =
+ previous?.request.id ??
+ (yield* idAllocator.allocate
+ .runtimeRequest({
+ driver: MUSE_PROVIDER,
+ ...(providerTurnId ? { providerTurnId } : {}),
+ nativeRequestId: nativeId,
+ })
+ .pipe(
+ Effect.mapError((cause) => protocolError("Cannot allocate Muse request", cause)),
+ ));
+ const time = yield* DateTime.now;
+ const nodeId = idAllocator.derive.approvalNode({ requestId });
+ const subject = native.type === "approval" ? native.value.subject : undefined;
+ const kind = !subject
+ ? "user_input"
+ : subject.kind === "shell"
+ ? "command"
+ : subject.kind !== "fileAccess"
+ ? "permission"
+ : subject.access === "read"
+ ? "file-read"
+ : "file-change";
+ const runtimeRequest: OrchestrationV2RuntimeRequest = {
+ id: requestId,
+ nodeId,
+ providerTurnId,
+ nativeRequestRef: nativeRef(nativeId),
+ kind,
+ status: "pending",
+ responseCapability: { type: "live", providerSessionId: input.providerSessionId },
+ createdAt: previous?.request.createdAt ?? time,
+ resolvedAt: null,
+ };
+ const node: OrchestrationV2ExecutionNode = {
+ id: nodeId,
+ threadId,
+ runId: turn?.input.runId ?? null,
+ parentNodeId: turn?.input.rootNodeId ?? null,
+ rootNodeId: turn?.input.rootNodeId ?? nodeId,
+ kind: native.type === "approval" ? "approval_request" : "user_input_request",
+ status: "waiting",
+ countsForRun: false,
+ providerThreadId: thread.id,
+ providerTurnId,
+ nativeItemRef: nativeRef(nativeId),
+ runtimeRequestId: requestId,
+ checkpointScopeId: null,
+ startedAt: runtimeRequest.createdAt,
+ completedAt: null,
+ };
+ const latestTurn = active?.providerTurn ?? lastProviderTurn;
+ const base = {
+ ...(turn
+ ? baseItem(turn, `request:${nativeId}`, time)
+ : {
+ threadId,
+ runId: null,
+ providerThreadId: thread.id,
+ providerTurnId: null,
+ nativeItemRef: nativeRef(nativeId),
+ parentItemId: null,
+ // Sorts after the latest turn's items.
+ ordinal: previous?.item.ordinal ?? ((latestTurn?.ordinal ?? 0) + 1) * 100,
+ startedAt: runtimeRequest.createdAt,
+ updatedAt: time,
+ }),
+ id: idAllocator.derive.approvalTurnItem({ requestId }),
+ nodeId,
+ status: "waiting" as const,
+ title: null,
+ completedAt: null,
+ };
+ const item: OrchestrationV2TurnItem =
+ native.type === "approval"
+ ? {
+ ...base,
+ type: "approval_request",
+ requestId,
+ requestKind: kind === "user_input" ? "command" : kind,
+ prompt:
+ native.value.subject.command ??
+ native.value.subject.path ??
+ native.value.subject.host ??
+ native.value.subject.target ??
+ native.value.toolName ??
+ "Muse requests permission",
+ options: museApprovalOptions(native.value),
+ }
+ : {
+ ...base,
+ type: "user_input_request",
+ requestId,
+ questions: native.value.questions.map((question) => ({
+ id: question.id,
+ header: question.header || "Question",
+ question: question.question,
+ options: question.options.map((option) => ({
+ label: option.label,
+ description: option.description || option.label,
+ })),
+ multiSelect: question.selection.mode === "multiple",
+ allowCustomAnswer: true,
+ required: true,
+ })),
+ };
+ const entry: PendingRequest = { native, request: runtimeRequest, node, item };
+ pending.set(requestId, entry);
+ yield* emit({
+ type: "runtime_request.updated",
+ driver: MUSE_PROVIDER,
+ threadId,
+ runtimeRequest,
+ });
+ yield* emit({ type: "node.updated", driver: MUSE_PROVIDER, node });
+ yield* emit({ type: "turn_item.updated", driver: MUSE_PROVIDER, turnItem: item });
+ yield* updateSession("waiting");
+ // Muse does not give workflow children the session's allowAll mode, so in full
+ // access T3 approves for them. If Muse rejects that, the request waits for the user.
+ if (child && input.runtimePolicy.runtimeMode === "full-access")
+ yield* decideApproval(entry, "accept").pipe(Effect.ignore, Effect.forkIn(scope));
+ });
+ /** Approval and question events; `turn` is undefined for a workflow child's approval. */
+ const handleRequestEvent = Effect.fnUntraced(function* (
+ method: string,
+ params: Record,
+ turn: ActiveTurn | undefined,
+ ) {
+ switch (method) {
+ case "approval/requested":
+ case "approval/updated":
+ if (
+ method === "approval/updated" &&
+ ![...pending.values()].some(
+ (entry) =>
+ entry.native.type === "approval" &&
+ entry.native.value.approvalId === params.approvalId,
+ )
+ )
+ return;
+ return yield* publishRequest({
+ type: "approval",
+ value: yield* decode(MuseApproval, params),
+ });
+ case "userInput/requested":
+ return yield* publishRequest({
+ type: "question",
+ value: yield* decode(MuseUserInput, params),
+ });
+ case "approval/resolved":
+ case "userInput/settled": {
+ const id = method === "approval/resolved" ? params.approvalId : params.userInputId;
+ if (typeof id === "string")
+ turn?.settledRequests.add(
+ `${method === "approval/resolved" ? "approval" : "question"}:${id}`,
+ );
+ const entry = [...pending.values()].find(
+ (candidate) => candidate.request.nativeRequestRef?.nativeId === id,
+ );
+ if (entry) {
+ // An approval settled outside T3 still shows Muse's own decision.
+ const native =
+ method === "approval/resolved" && typeof params.decision === "string"
+ ? museApprovalDecision({ decision: params.decision, scope: "once" })
+ : undefined;
+ if (!entry.response && native) entry.response = { decision: native };
+ yield* resolvePending(entry, "resolved");
+ }
+ if (!pending.size) yield* updateSession(active ? "running" : "ready");
+ }
+ }
+ });
+ const handleNotification = Effect.fnUntraced(function* (method: string, data: unknown) {
+ const params = yield* decode(recordSchema, data);
+ if (params.sessionId !== nativeSessionId) return;
+ if (method === "view/gap")
+ return yield* protocolError("Muse delivery gap requires session recovery");
+ if (method === "session/contextUsage") {
+ const usage = yield* decode(MuseContextUsage, params);
+ const currentTurn = active?.providerTurn ?? lastProviderTurn;
+ const updatedAt = DateTime.formatIso(yield* DateTime.now);
+ const tokenUsage = {
+ ...currentTurn?.tokenUsage,
+ usedTokens: usage.usedTokens,
+ maxTokens: usage.windowTokens ?? currentTurn?.tokenUsage?.maxTokens ?? undefined,
+ updatedAt,
+ };
+ yield* updateThread({ contextUsage: tokenUsage });
+ if (currentTurn) {
+ const updated = { ...currentTurn, tokenUsage };
+ if (active) active.providerTurn = updated;
+ else lastProviderTurn = updated;
+ yield* emit({
+ type: "provider_turn.updated",
+ driver: MUSE_PROVIDER,
+ providerTurn: updated,
+ });
+ }
+ return;
+ }
+ const itemEvent = ["item/started", "item/updated", "item/completed"].includes(method)
+ ? (yield* decode(MuseItemEvent, params)).item
+ : undefined;
+ if (itemEvent) {
+ const owner = observedChildren.get(itemEvent.itemId);
+ if (owner && owner !== active && itemEvent.turnId === owner.nativeId) {
+ const previous = owner.items.get(itemEvent.itemId);
+ if (!previous || itemEvent.revision > previous.revision) {
+ owner.items.set(itemEvent.itemId, itemEvent);
+ yield* publishItem(owner, itemEvent);
+ }
+ return;
+ }
+ }
+ // A workflow child's approval comes under the child's own run id, during a turn
+ // or after it, so it skips turn routing.
+ if (params.subagentOrigin !== undefined && method.startsWith("approval/"))
+ return yield* handleRequestEvent(method, params, undefined);
+ // Muse starts a turn on its own when a workflow finishes, to report its result.
+ // During a run, that run shows it. Otherwise hold it and ask the orchestrator for
+ // a run; that run takes the held events, unless a user turn takes them first.
+ if (
+ method === "turn/started" &&
+ typeof params.turnId === "string" &&
+ params.turnId !== active?.nativeId &&
+ params.turnId !== wake?.nativeId
+ ) {
+ if (active && !active.compact) active.joined.add(params.turnId);
+ else if (!active && !wake) {
+ const held = { nativeId: params.turnId, events: [] };
+ wake = held;
+ const notification = backgroundWorkNotification(finishedBackground.splice(0));
+ if (thread?.appThreadId && options.continuationRequests)
+ yield* options.continuationRequests.offer({
+ threadId: thread.appThreadId,
+ providerThreadId: thread.id,
+ driver: MUSE_PROVIDER,
+ detail: null,
+ ...(notification ? { notification } : {}),
+ dispatchIfCurrent: (dispatch) =>
+ wake === held ? Effect.map(dispatch, Option.some) : Effect.succeed(Option.none()),
+ clearIfCurrent: () =>
+ Effect.sync(() => {
+ if (wake === held) wake = undefined;
+ }),
+ });
+ }
+ }
+ if (wake && active?.nativeId !== wake.nativeId) {
+ const turnId = typeof params.turnId === "string" ? params.turnId : itemEvent?.turnId;
+ if (turnId === wake.nativeId) {
+ wake.events.push([method, data]);
+ return;
+ }
+ }
+ const turn = active;
+ if (!turn) return;
+ if (typeof params.turnId === "string" && !owns(turn, params.turnId) && !turn.compact)
+ return;
+ switch (method) {
+ case "item/started":
+ case "item/updated":
+ case "item/completed": {
+ const item = itemEvent!;
+ if (
+ item.turnId &&
+ !owns(turn, item.turnId) &&
+ !(turn.compact && item.kind === "compaction")
+ )
+ return;
+ const previous = turn.items.get(item.itemId);
+ if (previous && previous.revision >= item.revision) return;
+ turn.items.set(item.itemId, item);
+ turn.dirty.delete(item.itemId);
+ yield* publishItem(turn, item);
+ if (turn.compact && item.kind === "compaction" && item.status !== "inProgress") {
+ yield* finish(
+ turn,
+ item.status === "completed" && (!item.outcome || item.outcome === "compacted")
+ ? "completed"
+ : item.status === "cancelled"
+ ? "cancelled"
+ : "failed",
+ item.failureReason ?? item.reason,
+ );
+ }
+ break;
+ }
+ case "item/delta": {
+ const delta = yield* decode(MuseDelta, params);
+ const previous = turn.items.get(delta.itemId);
+ if (!previous || previous.status !== "inProgress") return;
+ const field = delta.field ?? "text";
+ let item = previous;
+ if (field === "text") item = { ...previous, text: (previous.text ?? "") + delta.delta };
+ else if (field === "output" || field === "visibleOutput")
+ item = { ...previous, visibleOutput: (previous.visibleOutput ?? "") + delta.delta };
+ else if (field.startsWith("summary.")) {
+ const index = Number(field.slice(8));
+ if (Number.isSafeInteger(index) && index >= 0 && index < 100) {
+ const summary = [...(previous.summary ?? [])];
+ summary[index] = (summary[index] ?? "") + delta.delta;
+ item = { ...previous, summary };
+ }
+ }
+ if (item === previous) return;
+ turn.items.set(item.itemId, item);
+ turn.dirty.add(item.itemId);
+ if (!turn.flushScheduled) {
+ turn.flushScheduled = true;
+ yield* Effect.sleep(50).pipe(
+ Effect.andThen(Queue.offer(inbox, { type: "flush", turn })),
+ Effect.forkIn(scope),
+ );
+ }
+ break;
+ }
+ case "turn/completed": {
+ if (turn.compact) return;
+ const result = yield* decode(MuseTurnCompleted, params);
+ // A joined Muse turn ending does not end this run; its own turn does.
+ if (result.turnId !== turn.nativeId) break;
+ if (result.usage) {
+ const usage = result.usage;
+ turn.providerTurn = {
+ ...turn.providerTurn,
+ turnTokenUsage: {
+ usageScope: "main_agent",
+ usageStatus: "complete",
+ hasSubagents: [...turn.items.values()].some((item) => item.kind === "subagent"),
+ inputTokens: turn.providerTurn.turnTokenUsage?.inputTokens ?? usage.inputTokens,
+ outputTokens: usage.outputTokens,
+ cachedInputTokens: usage.cacheReadTokens ?? usage.cachedTokens,
+ reasoningTokens: usage.reasoningTokens,
+ ...(usage.cacheWriteTokens === undefined
+ ? {}
+ : { cacheCreationTokens: usage.cacheWriteTokens }),
+ },
+ };
+ }
+ const status =
+ result.terminal === "completed"
+ ? "completed"
+ : result.terminal === "cancelled"
+ ? turn.interruptRequested
+ ? "interrupted"
+ : "cancelled"
+ : "failed";
+ yield* finish(turn, status, result.error?.message ?? result.reason);
+ break;
+ }
+ case "approval/requested":
+ case "approval/updated":
+ case "approval/resolved":
+ case "userInput/requested":
+ case "userInput/settled":
+ yield* handleRequestEvent(method, params, turn);
+ break;
+ case "session/tokenUsage": {
+ const usage = yield* decode(MuseTokenUsageEvent, params);
+ const previous = turn.providerTurn.turnTokenUsage;
+ const contextUsage = thread?.contextUsage;
+ turn.providerTurn = {
+ ...turn.providerTurn,
+ ...(contextUsage
+ ? {
+ tokenUsage: {
+ ...turn.providerTurn.tokenUsage,
+ usedTokens: contextUsage.usedTokens,
+ inputTokens: usage.usage.inputTokens,
+ outputTokens: usage.usage.outputTokens,
+ cachedInputTokens: usage.usage.cacheReadTokens ?? usage.usage.cachedTokens,
+ reasoningOutputTokens: usage.usage.reasoningTokens,
+ updatedAt: DateTime.formatIso(yield* DateTime.now),
+ },
+ }
+ : {}),
+ turnTokenUsage: {
+ usageScope: "main_agent",
+ usageStatus: "partial",
+ hasSubagents: [...turn.items.values()].some((item) => item.kind === "subagent"),
+ inputTokens: (previous?.inputTokens ?? 0) + usage.promptTokens,
+ outputTokens: (previous?.outputTokens ?? 0) + usage.usage.outputTokens,
+ cachedInputTokens:
+ (previous?.cachedInputTokens ?? 0) +
+ (usage.usage.cacheReadTokens ?? usage.usage.cachedTokens),
+ ...(usage.usage.cacheWriteTokens === undefined &&
+ previous?.cacheCreationTokens === undefined
+ ? {}
+ : {
+ cacheCreationTokens:
+ (previous?.cacheCreationTokens ?? 0) + (usage.usage.cacheWriteTokens ?? 0),
+ }),
+ reasoningTokens: (previous?.reasoningTokens ?? 0) + usage.usage.reasoningTokens,
+ },
+ };
+ yield* emit({
+ type: "provider_turn.updated",
+ driver: MUSE_PROVIDER,
+ providerTurn: turn.providerTurn,
+ });
+ break;
+ }
+ case "session/todoListChanged": {
+ const todo = yield* decode(MuseTodoList, params);
+ const steps: ReadonlyArray = todo.items
+ .filter((item) => item.text.trim() && item.status !== "cancelled")
+ .map((item, index) => ({
+ id: `${turn.nativeId}:todo:${index}`,
+ text: item.text.trim(),
+ status:
+ item.status === "completed"
+ ? "completed"
+ : item.status === "inProgress"
+ ? "running"
+ : "pending",
+ }));
+ const time = yield* DateTime.now;
+ const base = baseItem(turn, "todo", time);
+ const planId =
+ turn.todoPlanId ??
+ (yield* idAllocator.allocate
+ .plan({
+ threadId: turn.input.threadId,
+ runId: turn.input.runId,
+ driver: MUSE_PROVIDER,
+ })
+ .pipe(
+ Effect.mapError((cause) =>
+ protocolError("Cannot allocate Muse todo list", cause),
+ ),
+ ));
+ turn.todoPlanId = planId;
+ yield* emit({
+ type: "plan.updated",
+ driver: MUSE_PROVIDER,
+ plan: {
+ id: planId,
+ threadId: turn.input.threadId,
+ runId: turn.input.runId,
+ nodeId: base.nodeId,
+ kind: "todo_list",
+ status: steps.every((step) => step.status === "completed") ? "completed" : "active",
+ steps,
+ },
+ });
+ yield* emit({
+ type: "node.updated",
+ driver: MUSE_PROVIDER,
+ node: {
+ id: base.nodeId,
+ threadId: base.threadId,
+ runId: base.runId,
+ parentNodeId: turn.input.rootNodeId,
+ rootNodeId: turn.input.rootNodeId,
+ kind: "todo_list",
+ status: "completed",
+ countsForRun: false,
+ providerThreadId: base.providerThreadId,
+ providerTurnId: base.providerTurnId,
+ nativeItemRef: null,
+ runtimeRequestId: null,
+ checkpointScopeId: null,
+ startedAt: base.startedAt,
+ completedAt: time,
+ },
+ });
+ yield* emit({
+ type: "turn_item.updated",
+ driver: MUSE_PROVIDER,
+ turnItem: {
+ ...base,
+ nativeItemRef: null,
+ type: "todo_list",
+ status: "completed",
+ title: null,
+ completedAt: time,
+ planId,
+ steps,
+ },
+ });
+ break;
+ }
+ case "turn/retryScheduled": {
+ const retry = yield* decode(MuseTurnRetryScheduled, params);
+ const time = yield* DateTime.now;
+ yield* emit({
+ type: "turn_item.updated",
+ driver: MUSE_PROVIDER,
+ turnItem: {
+ ...baseItem(turn, `retry:${retry.nextAttempt}`, time),
+ type: "system_notice",
+ status: "completed",
+ title: "Muse retry",
+ completedAt: time,
+ message: `Muse is retrying this turn (attempt ${retry.nextAttempt} of ${retry.maxAttempts}): ${retry.reason}`,
+ },
+ });
+ break;
+ }
+ }
+ });
+ yield* Stream.fromQueue(inbox).pipe(
+ Stream.runForEach((entry) =>
+ Effect.gen(function* () {
+ if (entry.type === "flush") {
+ const turn = entry.turn;
+ turn.flushScheduled = false;
+ if (active !== turn) return;
+ for (const id of turn.dirty) {
+ const item = turn.items.get(id);
+ if (item) yield* publishItem(turn, item);
+ }
+ turn.dirty.clear();
+ } else if (entry.epoch === hostEpoch) {
+ if (entry.type === "failure") yield* failHost(entry.cause);
+ else {
+ if (options.nativeEventLogger && entry.method !== "item/delta")
+ yield* options.nativeEventLogger.write(
+ { provider: "muse", method: entry.method, params: entry.params },
+ input.threadId,
+ );
+ yield* handleNotification(entry.method, entry.params).pipe(
+ Effect.catch((cause) =>
+ INFORMATIONAL_NOTIFICATIONS.has(entry.method)
+ ? Effect.logWarning(`Skipped an unreadable Muse ${entry.method}`, cause)
+ : failHost(cause),
+ ),
+ );
+ }
+ }
+ }).pipe(eventPermit.withPermits(1)),
+ ),
+ Effect.forkIn(scope),
+ );
+ const launchHost = Effect.fnUntraced(function* () {
+ const epoch = ++hostEpoch;
+ const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
+ const created = yield* Effect.acquireRelease(
+ createMuseSdkHostEffect(
+ {
+ binaryPath: options.settings.binaryPath || "muse",
+ cwd,
+ environment: McpProviderSession.withAgentDeviceEnvironment(
+ options.environment,
+ mcpSession,
+ ),
+ runtimeMode: input.runtimePolicy.runtimeMode,
+ },
+ options.createHost,
+ ).pipe(
+ Effect.mapError(
+ (error) =>
+ new ProviderAdapterOpenSessionError({
+ driver: MUSE_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ cause: error.cause,
+ }),
+ ),
+ ),
+ (created) =>
+ Effect.gen(function* () {
+ if (host === created) {
+ closed = true;
+ hostEpoch++;
+ }
+ yield* Effect.tryPromise(() => created.close()).pipe(Effect.ignore);
+ }),
+ { interruptible: true },
+ ).pipe(Effect.provideService(Scope.Scope, scope));
+ host = created;
+ created.connection.onNotification((notification) => {
+ Queue.offerUnsafe(inbox, {
+ type: "notification",
+ method: notification.method,
+ params: notification.params,
+ epoch,
+ });
+ });
+ created.connection.onProtocolError((cause) => {
+ Queue.offerUnsafe(inbox, { type: "failure", cause, epoch });
+ });
+ // These are presentation receipts only. The matching notifications drive the UI,
+ // and decisions travel as `approval/decide` and `userInput/answer` commands.
+ created.connection.onServerRequest(async (request) => {
+ if (request.method === "approval/request" || request.method === "userInput/request")
+ return {};
+ throw new Error(`Unsupported Muse server request: ${request.method}`);
+ });
+ void created.connection.closed.then(() => {
+ if (!closed && epoch === hostEpoch)
+ Queue.offerUnsafe(inbox, {
+ type: "failure",
+ cause: new Error("Muse connection closed unexpectedly"),
+ epoch,
+ });
+ });
+ void created.exited.then((exit) => {
+ if (!closed && epoch === hostEpoch) {
+ const reason = created
+ .stderrTail?.()
+ .findLast((line) => line.trim())
+ ?.trim();
+ Queue.offerUnsafe(inbox, {
+ type: "failure",
+ cause: new Error(
+ `Muse exited unexpectedly (${exit.code ?? exit.signal})${reason ? `: ${reason}` : ""}`,
+ ),
+ epoch,
+ });
+ }
+ });
+ });
+ yield* Effect.addFinalizer(() =>
+ Effect.gen(function* () {
+ closed = true;
+ hostEpoch++;
+ if (active) yield* finish(active, "cancelled", "Muse session closed", "broken");
+ yield* settleObservedChildren("cancelled");
+ yield* Queue.shutdown(inbox);
+ yield* Queue.shutdown(events);
+ }).pipe(eventPermit.withPermits(1)),
+ );
+ yield* launchHost();
+
+ const register = Effect.fnUntraced(function* (args: ProviderAdapterV2EnsureThreadInput) {
+ if (broken || closed)
+ return yield* protocolError("Muse host is unavailable; reopen the session");
+ if (active)
+ return yield* protocolError("Cannot change Muse sessions during an active turn");
+ const existing = args.existingProviderThread;
+ if (
+ existing &&
+ (existing.driver !== MUSE_PROVIDER || existing.providerInstanceId !== options.instanceId)
+ )
+ return yield* protocolError("Muse thread belongs to another provider instance");
+ const requestedId = existing
+ ? (existing.nativeThreadRef?.nativeId ?? undefined)
+ : input.initialNativeThreadId;
+ if (
+ thread &&
+ (!requestedId || requestedId === nativeSessionId) &&
+ thread.appThreadId === args.threadId
+ )
+ return thread;
+ if (thread) return yield* protocolError("Open a new host to attach another Muse session");
+ let missingNativeSession = false;
+ return yield* Effect.gen(function* () {
+ nativeSessionId = requestedId ?? host.connection.mintCommandId();
+ const mcpSession = McpProviderSession.readMcpProviderSession(args.threadId);
+ if (mcpSession && !host.initializeResult.grantedCapabilities.includes("sessionMcp"))
+ return yield* protocolError(
+ "Update Muse Code to a version that supports session MCP servers",
+ );
+ const config = mcpSession
+ ? {
+ mcpServers: {
+ // Muse defaults to "required", which fails the whole run when T3's
+ // tools cannot be reached. The agent should still work without them.
+ "t3-code": {
+ transport: "streamableHttp",
+ mode: "optional",
+ url: mcpSession.endpoint,
+ headers: { Authorization: mcpSession.authorizationHeader },
+ },
+ },
+ }
+ : undefined;
+ const startModel = yield* resolveModel(args.modelSelection);
+ const result = yield* request(
+ requestedId ? "session/resume" : "session/start",
+ requestedId
+ ? // T3 already has the transcript; skip Muse's history payload.
+ { excludeItems: true, ...(config ? { config } : {}) }
+ : {
+ workspaceRoot: cwd,
+ ...(config ? { config } : {}),
+ ...(startModel ? { modelId: startModel } : {}),
+ providerId: "meta",
+ approvalMode: museApprovalMode(args.runtimePolicy.runtimeMode),
+ },
+ ).pipe(
+ Effect.catch((error) => {
+ missingNativeSession =
+ requestedId !== undefined &&
+ error.payload instanceof MspError &&
+ (error.payload.kind === "sessionNotFound" || error.payload.kind === "notFound");
+ return Effect.fail(error);
+ }),
+ Effect.flatMap((value) => decode(MuseSessionResult, value)),
+ );
+ if (result.session.sessionId !== nativeSessionId)
+ return yield* protocolError("Muse returned a different session identity");
+ if (requestedId)
+ yield* request("session/setApprovalMode", {
+ mode: museApprovalMode(args.runtimePolicy.runtimeMode),
+ });
+ // A resumed session keeps the model it last ran with; the next turn
+ // switches it when the selection differs.
+ const createdAt = yield* DateTime.now;
+ thread = existing
+ ? {
+ ...existing,
+ providerSessionId: input.providerSessionId,
+ nativeThreadRef: nativeRef(result.session.sessionId),
+ status: "idle",
+ updatedAt: createdAt,
+ }
+ : {
+ id: idAllocator.derive.providerThread({
+ driver: MUSE_PROVIDER,
+ providerInstanceId: options.instanceId,
+ nativeThreadId: nativeSessionId,
+ }),
+ driver: MUSE_PROVIDER,
+ providerInstanceId: options.instanceId,
+ providerSessionId: input.providerSessionId,
+ appThreadId: args.threadId,
+ ownerNodeId: null,
+ nativeThreadRef: nativeRef(nativeSessionId),
+ nativeConversationHeadRef: null,
+ status: "idle",
+ firstRunOrdinal: null,
+ lastRunOrdinal: null,
+ handoffIds: [],
+ forkedFrom: null,
+ createdAt,
+ updatedAt: createdAt,
+ };
+ session = {
+ ...session,
+ model: result.session.modelId ?? args.modelSelection.model,
+ };
+ yield* emit({
+ type: "provider_thread.updated",
+ driver: MUSE_PROVIDER,
+ providerThread: thread,
+ });
+ return thread;
+ }).pipe(
+ Effect.onError((cause) =>
+ missingNativeSession ? Effect.void : eventPermit.withPermits(1)(failHost(cause)),
+ ),
+ );
+ });
+ const prompt = Effect.fnUntraced(function* (message: ProviderAdapterV2TurnMessage) {
+ const parts: SendUserTurnOptions["input"] = [];
+ const text = providerMessageTextWithAttachmentPaths({
+ text: message.text,
+ attachments: message.attachments,
+ resolveAttachmentPath: options.host.resolveAttachmentPath,
+ });
+ if (text) parts.push({ type: "text", text });
+ for (const attachment of message.attachments)
+ if (isProviderNativeImageAttachment(attachment)) {
+ const path = options.host.resolveAttachmentPath(attachment);
+ if (!path) return yield* protocolError("Muse image attachment is missing");
+ const bytes = yield* options.fileSystem
+ .readFile(path)
+ .pipe(Effect.mapError((cause) => protocolError("Cannot read Muse image", cause)));
+ parts.push({
+ type: "image",
+ base64Data: Buffer.from(bytes).toString("base64"),
+ mediaType: attachment.mimeType,
+ });
+ }
+ if (!parts.length) return yield* protocolError("Muse needs text or an image");
+ return parts;
+ });
+ const validateThread = (candidate: OrchestrationV2ProviderThread) =>
+ thread !== undefined &&
+ candidate.driver === MUSE_PROVIDER &&
+ candidate.providerInstanceId === options.instanceId &&
+ candidate.appThreadId === thread.appThreadId &&
+ candidate.nativeThreadRef?.nativeId === nativeSessionId;
+ const selectionEffort = Effect.fnUntraced(function* (selection: ModelSelection) {
+ if (selection.instanceId !== options.instanceId)
+ return yield* protocolError("Model selection belongs to another Muse instance");
+ const catalog = options.modelCatalog ? yield* options.modelCatalog : [];
+ const model = yield* resolveModel(selection);
+ const selected = getModelSelectionStringOptionValue(selection, "reasoningEffort");
+ // With no saved choice, the model's own default applies.
+ return resolveMuseReasoningEffort(
+ catalog.find((entry) => entry.slug === model)?.capabilities ?? museModelCapabilities(),
+ selected,
+ );
+ });
+ const start = Effect.fnUntraced(function* (
+ turnInput: ProviderAdapterV2TurnInput,
+ compact = false,
+ ) {
+ if (!validateThread(turnInput.providerThread) || broken || closed)
+ return yield* protocolError("Muse thread is not attached to this host");
+ if (active)
+ return yield* protocolError(
+ "Muse already has an active turn; queue through the orchestrator",
+ );
+ if (turnInput.runtimePolicy.interactionMode === "plan")
+ return yield* protocolError("Muse does not support dedicated Plan mode");
+ if (turnInput.runtimePolicy.runtimeMode !== input.runtimePolicy.runtimeMode)
+ return yield* protocolError("Runtime policy changes require a fresh Muse host");
+ // A continuation run takes the turn Muse already started; it sends no prompt.
+ const continuation =
+ turnInput.message.createdBy === "agent" &&
+ turnInput.message.creationSource === "provider";
+ const adopted = continuation ? wake : undefined;
+ const effort = continuation ? undefined : yield* selectionEffort(turnInput.modelSelection);
+ const parts = compact || continuation ? [] : yield* prompt(turnInput.message);
+ const targetModel = continuation
+ ? undefined
+ : yield* resolveModel(turnInput.modelSelection);
+ if (targetModel && session.model !== targetModel) {
+ yield* request("session/setModel", {
+ model: { modelId: targetModel, providerId: "meta" },
+ });
+ session = { ...session, model: targetModel };
+ }
+ const nativeId = adopted?.nativeId ?? host.connection.mintCommandId();
+ const startedAt = yield* DateTime.now;
+ const providerTurn: OrchestrationV2ProviderTurn = {
+ id: idAllocator.derive.providerTurn({
+ driver: MUSE_PROVIDER,
+ nativeTurnId: `${options.instanceId}:${nativeSessionId}:${nativeId}`,
+ }),
+ providerThreadId: turnInput.providerThread.id,
+ nodeId: turnInput.rootNodeId,
+ runAttemptId: turnInput.attemptId,
+ nativeTurnRef: compact
+ ? { ...nativeRef(nativeId), strength: "weak" }
+ : nativeRef(nativeId),
+ ordinal: turnInput.providerTurnOrdinal,
+ status: "running",
+ startedAt,
+ completedAt: null,
+ };
+ const turn: ActiveTurn = {
+ input: turnInput,
+ providerTurn,
+ nativeId,
+ joined: new Set(),
+ items: new Map(),
+ ordinals: new Map(),
+ started: new Map(),
+ dirty: new Set(),
+ settledRequests: new Set(),
+ done: yield* Deferred.make(),
+ nextOrdinal: turnInput.providerTurnOrdinal * 100 + 1,
+ flushScheduled: false,
+ interruptRequested: false,
+ compact,
+ };
+ yield* Effect.gen(function* () {
+ thread = turnInput.providerThread;
+ active = turn;
+ yield* emit({
+ type: "provider_turn.updated",
+ driver: MUSE_PROVIDER,
+ threadId: turnInput.threadId,
+ providerTurn,
+ });
+ yield* updateThread({
+ status: "active",
+ firstRunOrdinal: thread.firstRunOrdinal ?? turnInput.runOrdinal,
+ lastRunOrdinal: turnInput.runOrdinal,
+ });
+ yield* updateSession("running");
+ // A user turn takes a held Muse turn: Muse runs it first, so this run shows it
+ // (and its approvals), and the continuation asked for it is no longer needed.
+ const held = wake;
+ wake = undefined;
+ if (continuation && !held) return yield* finish(turn, "completed");
+ if (held && !continuation) turn.joined.add(held.nativeId);
+ for (const [method, data] of held?.events ?? [])
+ yield* handleNotification(method, data).pipe(Effect.catch(failHost));
+ }).pipe(eventPermit.withPermits(1));
+ if (continuation) return;
+ yield* Effect.gen(function* () {
+ if (compact) {
+ const result = yield* request("session/compact", {}, true, nativeId).pipe(
+ Effect.flatMap((value) => decode(MuseCompactResult, value)),
+ );
+ if (result.status === "noop")
+ yield* eventPermit.withPermits(1)(
+ finish(turn, "failed", result.reason ?? "Muse has no context to compact."),
+ );
+ else if (result.status !== "accepted")
+ return yield* protocolError(
+ `Muse returned an unsupported compaction status: ${result.status}`,
+ );
+ } else {
+ const result = yield* request(
+ "turn/start",
+ {
+ input: [
+ {
+ type: "text",
+ text: buildRuntimeInstructions({
+ harness: "Muse Code",
+ model: session.model ?? undefined,
+ reasoningEffort: effort,
+ }),
+ },
+ ...parts,
+ ],
+ displayText: turnInput.message.text || "Image attachment",
+ // A resumed session keeps the root it was created with; pin it to
+ // this thread's current checkout so edits land where T3 tracks them.
+ workspaceRoots: [cwd],
+ ifBusy: "queue",
+ ...(effort ? { reasoningEffort: effort } : {}),
+ },
+ true,
+ nativeId,
+ ).pipe(Effect.flatMap((value) => decode(MuseTurnStartResult, value)));
+ if (result.turnId !== nativeId) {
+ yield* eventPermit.withPermits(1)(
+ failHost(new Error("Muse admitted an unowned turn")),
+ );
+ return yield* protocolError("Muse admitted the turn under an unexpected identity");
+ }
+ }
+ }).pipe(
+ Effect.onError((cause) => {
+ const error = Cause.squash(cause);
+ const detail = isProtocolError(error)
+ ? error.detail
+ : error instanceof Error
+ ? error.message
+ : "Muse could not start the turn.";
+ return eventPermit.withPermits(1)(finish(turn, "failed", detail));
+ }),
+ );
+ });
+ // Only messages are read back; T3 owns turn and item history.
+ const snapshot = Effect.fnUntraced(function* (
+ current: OrchestrationV2ProviderThread,
+ ): Effect.fn.Return {
+ const result = yield* request("session/read", { excludeItems: false }, false).pipe(
+ Effect.flatMap((value) => decode(MuseSessionResult, value)),
+ );
+ if (result.session.sessionId !== nativeSessionId)
+ return yield* protocolError("Muse snapshot belongs to a different session");
+ const items = result.history?.items ?? result.history?.snapshot?.state.items ?? [];
+ const now = yield* DateTime.now;
+ return {
+ providerThread: current,
+ providerTurns: [],
+ messages: items.flatMap((item): OrchestrationV2ConversationMessage[] =>
+ item.kind === "agentMessage" || item.kind === "userMessage"
+ ? [
+ {
+ id: idAllocator.derive.messageFromProviderItem({
+ driver: MUSE_PROVIDER,
+ nativeItemId: `${options.instanceId}:${current.id}:${item.turnId ?? "history"}:${item.itemId}`,
+ }),
+ threadId: current.appThreadId ?? input.threadId,
+ runId: null,
+ nodeId: null,
+ role: item.kind === "agentMessage" ? "assistant" : "user",
+ text: item.text ?? "",
+ attachments: [],
+ streaming: item.status === "inProgress",
+ createdBy: item.kind === "agentMessage" ? "agent" : "user",
+ creationSource: "provider",
+ createdAt: now,
+ updatedAt: now,
+ },
+ ]
+ : [],
+ ),
+ runtimeRequests: [...pending.values()].map((entry) => entry.request),
+ providerPayload: items,
+ };
+ });
+ const runtime: ProviderAdapterV2SessionRuntime = {
+ instanceId: options.instanceId,
+ driver: MUSE_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ providerSession: session,
+ events: Stream.fromQueue(events),
+ // A running workflow or a held Muse turn keeps the host: idle release must wait.
+ hasPendingBackgroundWork: Effect.sync(
+ () => observedChildren.size > 0 || wake !== undefined,
+ ),
+ ensureThread: (args) =>
+ register(args).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterEnsureThreadError({
+ driver: MUSE_PROVIDER,
+ threadId: args.threadId,
+ cause,
+ }),
+ ),
+ ),
+ resumeThread: (args) =>
+ register({
+ threadId: args.threadId ?? args.providerThread.appThreadId ?? input.threadId,
+ modelSelection: args.modelSelection ?? input.modelSelection,
+ runtimePolicy: args.runtimePolicy ?? input.runtimePolicy,
+ existingProviderThread: args.providerThread,
+ }).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterResumeThreadError({
+ driver: MUSE_PROVIDER,
+ providerSessionId: input.providerSessionId,
+ providerThreadId: args.providerThread.id,
+ cause,
+ }),
+ ),
+ ),
+ startTurn: (args) =>
+ start(args).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterTurnStartError({
+ driver: MUSE_PROVIDER,
+ threadId: args.threadId,
+ providerThreadId: args.providerThread.id,
+ runId: args.runId,
+ cause,
+ }),
+ ),
+ ),
+ compactThread: (args) => start(args, true).pipe(commands.withPermits(1)),
+ steerTurn: (args) =>
+ Effect.gen(function* () {
+ const turn = active;
+ if (
+ !turn ||
+ turn.providerTurn.id !== args.providerTurnId ||
+ !validateThread(args.providerThread) ||
+ turn.compact
+ )
+ return yield* protocolError("This Muse turn cannot be steered");
+ const parts = yield* prompt(args.message);
+ const result = yield* request("turn/steer", {
+ expectedTurnId: turn.nativeId,
+ input: parts,
+ }).pipe(Effect.flatMap((value) => decode(MuseTurnStartResult, value)));
+ if (result.turnId !== turn.nativeId)
+ return yield* protocolError("Muse steering targeted a different turn");
+ }).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterSteerRunError({
+ driver: MUSE_PROVIDER,
+ providerThreadId: args.providerThread.id,
+ providerTurnId: args.providerTurnId,
+ cause,
+ }),
+ ),
+ ),
+ interruptTurn: (args) =>
+ Effect.gen(function* () {
+ const turn = active;
+ if (
+ !turn ||
+ turn.providerTurn.id !== args.providerTurnId ||
+ !validateThread(args.providerThread)
+ )
+ return yield* protocolError("This Muse turn is no longer active");
+ turn.interruptRequested = true;
+ if (turn.compact) {
+ yield* eventPermit.withPermits(1)(
+ Effect.gen(function* () {
+ if (active !== turn) return;
+ hostEpoch++;
+ yield* Effect.tryPromise(() => host.close()).pipe(
+ Effect.mapError((cause) => protocolError("Cannot stop Muse compaction", cause)),
+ Effect.onError(failHost),
+ );
+ broken = true;
+ yield* finish(turn, "interrupted", undefined, "broken");
+ yield* settleObservedChildren("interrupted");
+ yield* updateSession("stopped");
+ yield* Queue.end(events);
+ }),
+ );
+ return;
+ }
+ // Muse runs a joined turn first, so Stop must end it too.
+ for (const joined of turn.joined)
+ yield* request("turn/interrupt", { turnId: joined }).pipe(Effect.ignore);
+ yield* request("turn/interrupt", { turnId: turn.nativeId });
+ yield* Deferred.await(turn.done).pipe(
+ Effect.timeout(options.requestTimeoutMs ?? 30_000),
+ Effect.mapError((cause) => protocolError("Muse did not confirm interruption", cause)),
+ Effect.onError((cause) => eventPermit.withPermits(1)(failHost(cause))),
+ );
+ }).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterInterruptError({
+ driver: MUSE_PROVIDER,
+ providerThreadId: args.providerThread.id,
+ providerTurnId: args.providerTurnId,
+ cause,
+ }),
+ ),
+ ),
+ respondToRuntimeRequest: (args) =>
+ Effect.gen(function* () {
+ const entry = pending.get(args.requestId);
+ if (!entry) return yield* protocolError("This Muse request is no longer pending");
+ if (entry.native.type === "approval") yield* decideApproval(entry, args.decision);
+ else {
+ const questionRequest = entry.native.value;
+ const answers = [];
+ for (const question of questionRequest.questions) {
+ const raw = yield* decode(responseAnswerSchema, args.answers?.[question.id]);
+ const values = typeof raw === "string" ? [raw] : [...new Set(raw)];
+ const selected = values.filter((value) =>
+ question.options.some((option) => option.label === value),
+ );
+ const freeText = values.filter((value) => !selected.includes(value)).join("\n");
+ if (freeText.length > 500 || (!freeText && !selected.length))
+ return yield* protocolError(
+ "Muse needs a nonempty answer with at most 500 custom characters",
+ );
+ if (
+ (question.selection.mode === "single" && selected.length > 1) ||
+ (selected.length > 0 &&
+ (selected.length < (question.selection.minSelections ?? 0) ||
+ selected.length > (question.selection.maxSelections ?? Infinity)))
+ )
+ return yield* protocolError("Muse question selection count is invalid");
+ answers.push({
+ questionId: question.id,
+ ...(selected.length
+ ? question.selection.mode === "single"
+ ? { selectedLabel: selected[0]! }
+ : { selectedLabels: selected }
+ : {}),
+ ...(freeText ? (selected.length ? { note: freeText } : { freeText }) : {}),
+ });
+ }
+ if (args.answers) entry.response = { answers: args.answers };
+ yield* request("userInput/answer", {
+ userInputId: questionRequest.userInputId,
+ answers,
+ }).pipe(Effect.tapError(() => Effect.sync(() => delete entry.response)));
+ }
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterRuntimeRequestResponseError({
+ driver: MUSE_PROVIDER,
+ requestId: args.requestId,
+ cause,
+ }),
+ ),
+ ),
+ readThreadSnapshot: (args) =>
+ (!validateThread(args.providerThread)
+ ? protocolError("Snapshot requested for a different Muse thread")
+ : snapshot(args.providerThread)
+ ).pipe(
+ commands.withPermits(1),
+ Effect.mapError(
+ (cause) =>
+ new ProviderAdapterReadThreadSnapshotError({
+ driver: MUSE_PROVIDER,
+ providerThreadId: args.providerThread.id,
+ cause,
+ }),
+ ),
+ ),
+ // Muse rejects forks at completed-turn boundaries (InvalidCut), so T3 uses
+ // portable context handoff for forks and does not rewind Muse conversations.
+ rollbackThread: (args) =>
+ Effect.fail(
+ new ProviderAdapterRollbackThreadError({
+ driver: MUSE_PROVIDER,
+ providerThreadId: args.providerThread.id,
+ checkpointId: args.target.checkpointId,
+ cause: "Muse Code does not support conversation rollback in T3 Code.",
+ }),
+ ),
+ forkThread: (args) =>
+ Effect.fail(
+ new ProviderAdapterForkThreadError({
+ driver: MUSE_PROVIDER,
+ providerThreadId: args.sourceProviderThread.id,
+ cause: "Muse Code does not support native forks in T3 Code.",
+ }),
+ ),
+ };
+ return runtime;
+ }),
+ });
+}
diff --git a/packages/provider-muse/src/server/driver.ts b/packages/provider-muse/src/server/driver.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87f6b2818907832260665d4d9f46936facd2c425
--- /dev/null
+++ b/packages/provider-muse/src/server/driver.ts
@@ -0,0 +1,178 @@
+import { ProviderDriverKind } from "@t3tools/contracts";
+import { MuseSettings } from "../settings.ts";
+import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+import { HttpClient } from "effect/http";
+import { ChildProcessSpawner } from "effect/process";
+
+import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost";
+import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
+import { makeMuseTextGeneration } from "./textGeneration.ts";
+import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
+import { makeMuseAdapterV2 } from "./adapter.ts";
+import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
+import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
+import { checkMuseProviderStatus, makePendingMuseProvider } from "./status.ts";
+import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
+import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
+import { enrichMuseSnapshot, latestMuseVersion, museMaintenance } from "./maintenance.ts";
+import { makeMuseEnvironment } from "./sdk.ts";
+import {
+ defaultProviderContinuationIdentity,
+ type ProviderDriver,
+ type ProviderInstance,
+} from "@t3tools/provider-core/server/driver";
+import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
+import {
+ makeCachedProviderMaintenanceResolution,
+ resolveProviderMaintenanceCapabilitiesEffect,
+} from "@t3tools/provider-core/server/maintenanceResolver";
+import {
+ haveProviderSnapshotSettingsChanged,
+ makeProviderSnapshotSettingsSource,
+ type ProviderSnapshotSettings,
+} from "@t3tools/provider-core/server/snapshotSettings";
+import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
+
+const DRIVER_KIND = ProviderDriverKind.make("muse");
+const decodeMuseSettings = Schema.decodeSync(MuseSettings);
+
+export type MuseDriverEnv =
+ | IdAllocator.IdAllocatorV2
+ | ProviderHost
+ | ChildProcessSpawner.ChildProcessSpawner
+ | FileSystem.FileSystem
+ | HttpClient.HttpClient
+ | Path.Path
+ | ProviderEventLoggers.ProviderEventLoggers;
+
+export const MuseDriver: ProviderDriver = {
+ driverKind: DRIVER_KIND,
+ metadata: { displayName: "Muse Code", supportsMultipleInstances: true },
+ configSchema: MuseSettings,
+ defaultConfig: () => decodeMuseSettings({}),
+ create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
+ Effect.gen(function* () {
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+ const fileSystem = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const httpClient = yield* HttpClient.HttpClient;
+ const host = yield* ProviderHost;
+ const eventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
+ const { cwd } = host.paths;
+ const idAllocator = yield* IdAllocator.IdAllocatorV2;
+ const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
+ const hostEnvironment = yield* HostProcessEnvironment;
+ // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins.
+ const processEnvironment = mergeProviderInstanceEnvironment(
+ environment,
+ makeMuseEnvironment(hostEnvironment),
+ );
+ const effectiveConfig = {
+ ...config,
+ enabled,
+ binaryPath: expandHomePath(config.binaryPath),
+ } satisfies MuseSettings;
+ const continuationIdentity = defaultProviderContinuationIdentity({
+ driverKind: DRIVER_KIND,
+ instanceId,
+ });
+ const stampIdentity = withInstanceIdentity({
+ instanceId,
+ driverKind: DRIVER_KIND,
+ displayName,
+ accentColor,
+ continuationGroupKey: continuationIdentity.continuationKey,
+ });
+ const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, host.settings);
+ const resolveInstallation = yield* makeCachedProviderMaintenanceResolution(
+ resolveProviderMaintenanceCapabilitiesEffect(museMaintenance, {
+ binaryPath: effectiveConfig.binaryPath,
+ env: processEnvironment,
+ }).pipe(
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
+ Effect.provideService(Path.Path, path),
+ ),
+ );
+ const resolveMaintenance = (options?: { readonly fresh?: boolean }) =>
+ Effect.gen(function* () {
+ const capabilities = yield* resolveInstallation(options);
+ // The maintenance runner requests fresh capabilities around an explicit update
+ // and needs the native target version to verify that the command actually upgraded.
+ const latestVersion = options?.fresh
+ ? yield* latestMuseVersion(processEnvironment, { fresh: true }).pipe(
+ Effect.provideService(HttpClient.HttpClient, httpClient),
+ )
+ : undefined;
+ return latestVersion !== undefined ? { ...capabilities, latestVersion } : capabilities;
+ });
+ const snapshot = yield* makeManagedServerProvider>({
+ resolveMaintenance,
+ getSettings: snapshotSettings.getSettings,
+ streamSettings: snapshotSettings.streamSettings,
+ haveSettingsChanged: haveProviderSnapshotSettingsChanged,
+ initialSnapshot: (settings) =>
+ makePendingMuseProvider(settings.provider).pipe(Effect.map(stampIdentity)),
+ checkProvider: checkMuseProviderStatus(effectiveConfig, processEnvironment, cwd).pipe(
+ Effect.map(stampIdentity),
+ Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+ Effect.provideService(FileSystem.FileSystem, fileSystem),
+ Effect.provideService(Path.Path, path),
+ ),
+ enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
+ resolveMaintenance().pipe(
+ Effect.flatMap((maintenanceCapabilities) =>
+ enrichMuseSnapshot({
+ snapshot: currentSnapshot,
+ maintenanceCapabilities,
+ enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
+ environment: processEnvironment,
+ }),
+ ),
+ Effect.provideService(HttpClient.HttpClient, httpClient),
+ Effect.flatMap(publishSnapshot),
+ ),
+ }).pipe(
+ Effect.mapError(
+ (cause) =>
+ new ProviderDriverError({
+ driver: DRIVER_KIND,
+ instanceId,
+ detail: "Failed to build Muse Code snapshot.",
+ cause,
+ }),
+ ),
+ );
+ const modelCatalog = snapshot.getSnapshot.pipe(Effect.map((current) => current.models));
+ const orchestrationAdapter = makeMuseAdapterV2({
+ instanceId,
+ settings: effectiveConfig,
+ environment: processEnvironment,
+ idAllocator,
+ host,
+ fileSystem,
+ modelCatalog,
+ ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
+ continuationRequests,
+ });
+ const textGeneration = yield* makeMuseTextGeneration(effectiveConfig, {
+ environment: processEnvironment,
+ modelCatalog,
+ });
+ return {
+ instanceId,
+ driverKind: DRIVER_KIND,
+ continuationIdentity,
+ displayName,
+ accentColor,
+ enabled,
+ snapshot,
+ orchestrationAdapter,
+ textGeneration,
+ } satisfies ProviderInstance;
+ }),
+};
diff --git a/packages/provider-muse/src/server/itemPresentation.test.ts b/packages/provider-muse/src/server/itemPresentation.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..59e48db95fd1652f0a3714913cd5733aa2466c67
--- /dev/null
+++ b/packages/provider-muse/src/server/itemPresentation.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it } from "@effect/vitest";
+
+import type { MuseItem } from "./protocol.ts";
+import { museItemStatus, museToolPresentation } from "./itemPresentation.ts";
+
+const item = (fields: Partial): MuseItem => ({
+ itemId: "native-item",
+ kind: "toolCall",
+ status: "completed",
+ revision: 1,
+ ...fields,
+});
+
+describe("Muse item presentation", () => {
+ it("preserves an edit's file and exact old/new text for the shared diff UI", () => {
+ expect(
+ museToolPresentation(
+ item({
+ tool: "edit_file",
+ args: JSON.stringify({ file_path: "src/index.ts", old_string: "old\n", new_string: "" }),
+ }),
+ ),
+ ).toMatchObject({
+ type: "file_change",
+ status: "completed",
+ fileName: "src/index.ts",
+ oldStr: "old\n",
+ newStr: "",
+ });
+ });
+
+ it("preserves write content and provider-supplied patches", () => {
+ expect(
+ museToolPresentation(
+ item({
+ tool: "write_file",
+ args: JSON.stringify({ path: "README.md", content: "Hello\n" }),
+ }),
+ ),
+ ).toMatchObject({ type: "file_change", newStr: "Hello\n" });
+ expect(
+ museToolPresentation(
+ item({
+ tool: "apply_patch",
+ args: JSON.stringify({ path: "README.md", patch: "@@ -1 +1 @@\n-old\n+new" }),
+ }),
+ ),
+ ).toMatchObject({ type: "file_change", diffStr: "@@ -1 +1 @@\n-old\n+new" });
+ });
+
+ it.each(["rejected", "timedOut", "handedOff"])(
+ "does not report native %s activity as successful",
+ (status) => {
+ const native = item({ tool: "search_files", status, fallbackText: "Native explanation" });
+ expect(museItemStatus(native)).toBe("failed");
+ expect(museToolPresentation(native)).toMatchObject({
+ status: "failed",
+ output: `Muse reported ${status}.\nNative explanation`,
+ });
+ },
+ );
+
+ it("preserves failure details and a known cancelled status", () => {
+ expect(
+ museToolPresentation(
+ item({
+ status: "failed",
+ failureReason: "Permission denied",
+ visibleOutput: "Partial output",
+ }),
+ ),
+ ).toMatchObject({ status: "failed", output: "Permission denied\nPartial output" });
+ expect(museItemStatus(item({ status: "cancelled" }))).toBe("cancelled");
+ });
+
+ it.each(["failed", "noop"])(
+ "keeps %s compaction outcomes from appearing completed",
+ (outcome) => {
+ expect(museItemStatus(item({ kind: "compaction", outcome }))).toBe("failed");
+ },
+ );
+});
diff --git a/packages/provider-muse/src/server/itemPresentation.ts b/packages/provider-muse/src/server/itemPresentation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e05490698bef0e7e37efdfef1b71ae346ec5b225
--- /dev/null
+++ b/packages/provider-muse/src/server/itemPresentation.ts
@@ -0,0 +1,117 @@
+import type { OrchestrationV2TurnItem, OrchestrationV2TurnItemStatus } from "@t3tools/contracts";
+import * as Schema from "effect/Schema";
+
+import type { MuseItem } from "./protocol.ts";
+
+const decodeToolArgs = Schema.decodeUnknownSync(
+ Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)),
+);
+
+type Fields<
+ T extends OrchestrationV2TurnItem["type"],
+ K extends keyof Extract,
+> = Pick, K>;
+type MuseToolPresentation = {
+ readonly status: OrchestrationV2TurnItemStatus;
+ readonly title: string | null;
+} & (
+ | Fields<"command_execution", "type" | "input" | "output" | "exitCode">
+ | Fields<"file_change", "type" | "fileName" | "oldStr" | "newStr" | "diffStr">
+ | Fields<"web_search", "type" | "patterns" | "results">
+ | Fields<"dynamic_tool", "type" | "toolName" | "input" | "output">
+);
+
+/** Unrecognized native terminal states must not appear as successful work. */
+export function museItemStatus(item: MuseItem): OrchestrationV2TurnItemStatus {
+ if (item.status === "inProgress") return "running";
+ if (item.status === "cancelled") return "cancelled";
+ if (
+ item.status === "completed" &&
+ !(item.kind === "compaction" && item.outcome && item.outcome !== "compacted")
+ )
+ return "completed";
+ return "failed";
+}
+
+/** Converts native tool details into the shared timeline fields used by every client. */
+export function museToolPresentation(
+ item: MuseItem,
+ status = museItemStatus(item),
+): MuseToolPresentation {
+ let args: Record | undefined;
+ if (item.args) {
+ try {
+ args = decodeToolArgs(item.args);
+ } catch {
+ // Streaming model-authored arguments may not be valid JSON yet.
+ }
+ }
+ const stringArg = (...keys: string[]) => {
+ for (const key of keys) {
+ const value = args?.[key];
+ if (typeof value === "string") return value;
+ }
+ return undefined;
+ };
+ const toolName = item.tool ?? item.kind;
+ const title =
+ item.kind === "subagent"
+ ? item.objective || item.role || "Muse agent"
+ : item.kind === "workflow"
+ ? item.entryId || "Workflow"
+ : toolName;
+ const detail =
+ status === "failed" ? item.failureReason || `Muse reported ${item.status}.` : undefined;
+ const shared = { status, title: detail ? `${title}: ${detail}` : title };
+ const text =
+ item.visibleOutput ??
+ item.result?.summary ??
+ item.text ??
+ item.summary?.join("\n") ??
+ item.fallbackText ??
+ item.objective ??
+ "";
+ const output = detail ? [detail, text].filter(Boolean).join("\n") : text;
+
+ if (item.kind === "userShell" || /shell|bash|exec_command/i.test(item.tool ?? "")) {
+ return {
+ ...shared,
+ type: "command_execution",
+ input: item.commandText ?? stringArg("command", "cmd") ?? item.args ?? "",
+ output,
+ ...(item.exitCode === undefined ? {} : { exitCode: item.exitCode }),
+ };
+ }
+ if (item.kind === "toolCall" && /write|edit|patch/i.test(toolName)) {
+ const fileName = stringArg("file_path", "path", "filePath")?.trim();
+ if (fileName) {
+ const oldStr = stringArg("old_string", "old_str", "oldStr", "oldText");
+ const newStr = stringArg("new_string", "new_str", "newStr", "newText", "content");
+ const diffStr = stringArg("diff", "patch", "diffStr");
+ return {
+ ...shared,
+ type: "file_change",
+ fileName,
+ ...(oldStr === undefined ? {} : { oldStr }),
+ ...(newStr === undefined ? {} : { newStr }),
+ ...(diffStr === undefined ? {} : { diffStr }),
+ };
+ }
+ }
+ if (item.kind === "toolCall" && /web|browse|fetch/i.test(toolName)) {
+ const query = stringArg("query", "url", "pattern")?.trim();
+ return {
+ ...shared,
+ type: "web_search",
+ ...(query ? { patterns: [query] } : {}),
+ ...(output ? { results: [{ snippet: output }] } : {}),
+ };
+ }
+ return {
+ ...shared,
+ type: "dynamic_tool",
+ toolName,
+ input: args ?? item.args ?? "",
+ output,
+ };
+}
diff --git a/packages/provider-muse/src/server/maintenance.test.ts b/packages/provider-muse/src/server/maintenance.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ac4a48cae602743128a6de93d33699ec725f084
--- /dev/null
+++ b/packages/provider-muse/src/server/maintenance.test.ts
@@ -0,0 +1,232 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { expect, it } from "@effect/vitest";
+import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import { HttpClient, HttpClientResponse } from "effect/http";
+
+import {
+ compareMuseVersions,
+ enrichMuseSnapshot,
+ museMaintenance,
+ latestMuseVersion,
+ parseMuseVersion,
+} from "./maintenance.ts";
+import {
+ ProviderVersionCache,
+ makeManualOnlyProviderMaintenanceCapabilities,
+} from "@t3tools/provider-core/server/maintenanceResolver";
+
+const maintenanceCapabilities = {
+ ...makeManualOnlyProviderMaintenanceCapabilities({
+ provider: ProviderDriverKind.make("muse"),
+ packageName: null,
+ }),
+ compareVersions: compareMuseVersions,
+};
+const snapshot: ServerProvider = {
+ instanceId: ProviderInstanceId.make("muse"),
+ driver: ProviderDriverKind.make("muse"),
+ enabled: true,
+ installed: true,
+ version: "1.0.3",
+ status: "ready",
+ auth: { status: "unknown" },
+ checkedAt: "2026-09-10T00:00:00.000Z",
+ models: [],
+ skills: [],
+ slashCommands: [],
+};
+
+it.layer(NodeServices.layer)("Muse maintenance", (it) => {
+ it.effect(
+ "refreshes a cached channel failure when an explicit update requests verification",
+ () =>
+ Effect.gen(function* () {
+ let requests = 0;
+ const check = Effect.gen(function* () {
+ expect(yield* latestMuseVersion({})).toBeNull();
+ expect(yield* latestMuseVersion({})).toBeNull();
+ expect(yield* latestMuseVersion({}, { fresh: true })).toBe("1.1.1-R10.1");
+ });
+ yield* check.pipe(
+ Effect.provideService(ProviderVersionCache, new Map()),
+ Effect.provideService(
+ HttpClient.HttpClient,
+ HttpClient.make((request) => {
+ requests += 1;
+ return Effect.succeed(
+ HttpClientResponse.fromWeb(
+ request,
+ requests === 1
+ ? new Response("unavailable", { status: 503 })
+ : Response.json({ version: "1.1.1-R10.1" }),
+ ),
+ );
+ }),
+ ),
+ );
+ expect(requests).toBe(2);
+ }),
+ );
+ it.effect(
+ "compares numeric native release revisions and preserves the CLI release identity",
+ () =>
+ Effect.sync(() => {
+ expect(parseMuseVersion("Muse Code 1.1.1 (1.1.1-R2514.1)")).toBe("1.1.1-R2514.1");
+ expect(compareMuseVersions("1.1.1-R9.1", "1.1.1-R10.1")).toBeLessThan(0);
+ expect(compareMuseVersions("1.1.1-R2514.1", "1.1.1-R2514.2")).toBeLessThan(0);
+ expect(compareMuseVersions("1.2.0-R1.1", "1.1.1-R2514.2")).toBeGreaterThan(0);
+ // A CLI that prints no revision compares by release, never as an older prerelease.
+ expect(compareMuseVersions("1.4.3", "1.4.3-R5018.1")).toBe(0);
+ expect(compareMuseVersions("1.4.2", "1.4.3-R5018.1")).toBeLessThan(0);
+ }),
+ );
+ it.effect(
+ "finds releases from the instance's channel and caches the native version, not the SDK version",
+ () =>
+ Effect.gen(function* () {
+ const requests: string[] = [];
+ const httpClient = HttpClient.make((request) => {
+ requests.push(request.url);
+ return Effect.succeed(
+ HttpClientResponse.fromWeb(request, Response.json({ version: "1.1.1-R2514.1" })),
+ );
+ });
+ const check = enrichMuseSnapshot({
+ snapshot,
+ maintenanceCapabilities,
+ enableProviderUpdateChecks: true,
+ environment: { MUSE_CHANNEL: "muse-canary" },
+ });
+ const [first, second] = yield* Effect.all([check, check], { concurrency: 1 }).pipe(
+ Effect.provideService(HttpClient.HttpClient, httpClient),
+ Effect.provideService(ProviderVersionCache, new Map()),
+ );
+ expect(first.versionAdvisory).toMatchObject({
+ status: "behind_latest",
+ currentVersion: "1.0.3",
+ latestVersion: "1.1.1-R2514.1",
+ canUpdate: false,
+ });
+ expect(second.versionAdvisory?.latestVersion).toBe("1.1.1-R2514.1");
+ expect(requests).toEqual(["https://api.meta.ai/muse-code/channels/muse-canary"]);
+ }),
+ );
+
+ it.effect("reports native release R9 behind R10 and R10 current against R9", () =>
+ Effect.gen(function* () {
+ for (const [current, latest, status] of [
+ ["1.4.2-R9.1", "1.4.2-R10.1", "behind_latest"],
+ ["1.4.2-R10.1", "1.4.2-R9.1", "current"],
+ ] as const) {
+ const result = yield* enrichMuseSnapshot({
+ snapshot: { ...snapshot, version: current },
+ maintenanceCapabilities,
+ enableProviderUpdateChecks: true,
+ environment: {},
+ }).pipe(
+ Effect.provideService(
+ HttpClient.HttpClient,
+ HttpClient.make((request) =>
+ Effect.succeed(
+ HttpClientResponse.fromWeb(request, Response.json({ version: latest })),
+ ),
+ ),
+ ),
+ Effect.provideService(ProviderVersionCache, new Map()),
+ );
+ expect(result.versionAdvisory?.status).toBe(status);
+ }
+ }),
+ );
+
+ it.effect("skips network checks when disabled, uninstalled or disabled in settings", () =>
+ Effect.gen(function* () {
+ for (const input of [
+ { snapshot, enableProviderUpdateChecks: false },
+ { snapshot: { ...snapshot, enabled: false }, enableProviderUpdateChecks: true },
+ { snapshot: { ...snapshot, installed: false }, enableProviderUpdateChecks: true },
+ ]) {
+ const result = yield* enrichMuseSnapshot({
+ ...input,
+ maintenanceCapabilities,
+ environment: {},
+ }).pipe(
+ Effect.provideService(
+ HttpClient.HttpClient,
+ HttpClient.make(() => Effect.die("Unexpected release request")),
+ ),
+ );
+ expect(result.versionAdvisory?.status).toBe("unknown");
+ }
+ }),
+ );
+
+ it.effect("tolerates invalid release manifests and unavailable channels", () =>
+ Effect.gen(function* () {
+ for (const response of [
+ Response.json({ version: 12 }),
+ Response.json({ version: "1.1.1" }),
+ new Response("unavailable", { status: 503 }),
+ ]) {
+ const result = yield* enrichMuseSnapshot({
+ snapshot,
+ maintenanceCapabilities,
+ enableProviderUpdateChecks: true,
+ environment: {},
+ }).pipe(
+ Effect.provideService(
+ HttpClient.HttpClient,
+ HttpClient.make((request) =>
+ Effect.succeed(HttpClientResponse.fromWeb(request, response)),
+ ),
+ ),
+ Effect.provideService(ProviderVersionCache, new Map()),
+ );
+ expect(result.status).toBe("ready");
+ expect(result.versionAdvisory?.status).toBe("unknown");
+ }
+ }),
+ );
+
+ it.effect("updates only a recognized launcher at the resolved instance path", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-maintenance-" });
+ const binaryPath = `${directory}/custom muse`;
+ const context = {
+ binaryPath,
+ resolvedCommandPath: binaryPath,
+ realCommandPath: binaryPath,
+ platform: "linux" as const,
+ env: { MUSE_CHANNEL: "muse-canary" },
+ };
+ yield* fs.writeFileString(
+ binaryPath,
+ "#!/bin/bash\n# muse-code/launcher-2\n# MUSE_SYNC_UPDATE\n",
+ );
+ const result = yield* museMaintenance.resolve(context);
+ expect(result.update).toMatchObject({
+ executable: binaryPath,
+ args: ["--version"],
+ lockKey: `muse:${binaryPath}`,
+ env: {
+ MUSE_CHANNEL: "muse-canary",
+ MUSE_NO_AUTO_UPDATE: "0",
+ MUSE_SYNC_UPDATE: "1",
+ MUSE_UPDATE_INTERVAL_SECONDS: "0",
+ },
+ });
+ expect(result.update?.command).toBe(
+ `MUSE_CHANNEL=muse-canary MUSE_NO_AUTO_UPDATE=0 MUSE_SYNC_UPDATE=1 MUSE_UPDATE_INTERVAL_SECONDS=0 '${binaryPath}' --version`,
+ );
+ expect(result.compareVersions).toBe(compareMuseVersions);
+ yield* fs.writeFileString(binaryPath, "standalone binary");
+ expect((yield* museMaintenance.resolve(context)).update).toBeNull();
+ expect((yield* museMaintenance.resolve(null)).update).toBeNull();
+ }),
+ ),
+ );
+});
diff --git a/packages/provider-muse/src/server/maintenance.ts b/packages/provider-muse/src/server/maintenance.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0770b494a819176657236f0ca502d1d83c8c8c42
--- /dev/null
+++ b/packages/provider-muse/src/server/maintenance.ts
@@ -0,0 +1,139 @@
+import { ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
+import { compareSemverVersions } from "@t3tools/shared/semver";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Schema from "effect/Schema";
+import { HttpClient, HttpClientRequest } from "effect/http";
+
+import {
+ enrichProviderSnapshotWithVersionAdvisory,
+ makeManualOnlyProviderMaintenanceCapabilities,
+ makeProviderMaintenanceCapabilities,
+ ProviderVersionCache,
+ type ProviderMaintenanceCapabilities,
+ type ProviderMaintenanceCapabilitiesResolver,
+} from "@t3tools/provider-core/server/maintenanceResolver";
+import { parseGenericCliVersion } from "@t3tools/provider-core/server/snapshotProbe";
+
+const DRIVER = ProviderDriverKind.make("muse");
+/** Mirrors the official launcher, which reads MUSE_CHANNEL and accepts only these two. */
+const MUSE_CHANNELS = new Set(["muse-stable", "muse-canary"]);
+const ChannelManifest = Schema.Struct({
+ version: Schema.String.check(Schema.isPattern(/^\d+\.\d+\.\d+-R\d+(?:\.\d+)?$/)),
+});
+
+/** Muse prints both a display version and a release revision; prefer the latter. */
+export function parseMuseVersion(output: string): string | null {
+ return output.match(/\b\d+\.\d+\.\d+-R\d+(?:\.\d+)?\b/)?.[0] ?? parseGenericCliVersion(output);
+}
+
+function comparableMuseVersion(version: string): string {
+ // Make the release sequence numeric for semver comparison (R9 precedes R10).
+ return version.replace(/-R(\d+)(?:\.(\d+))?$/, "-release.$1.$2").replace(/\.$/, ".0");
+}
+
+const MUSE_REVISION = /-R\d+(?:\.\d+)?$/;
+
+/** A version printed without a revision is compared by its release alone. */
+export const compareMuseVersions = (current: string, latest: string) =>
+ MUSE_REVISION.test(current) === MUSE_REVISION.test(latest)
+ ? compareSemverVersions(comparableMuseVersion(current), comparableMuseVersion(latest))
+ : compareSemverVersions(current.replace(MUSE_REVISION, ""), latest.replace(MUSE_REVISION, ""));
+
+/** Only the official launcher owns an update channel; a standalone binary cannot update itself. */
+export const museMaintenance: ProviderMaintenanceCapabilitiesResolver = {
+ resolve: (context) =>
+ Effect.gen(function* () {
+ const manual = {
+ ...makeManualOnlyProviderMaintenanceCapabilities({ provider: DRIVER, packageName: null }),
+ compareVersions: compareMuseVersions,
+ };
+ if (!context || context.platform === "win32") return manual;
+ const fs = yield* FileSystem.FileSystem;
+ const isLauncher = yield* Effect.gen(function* () {
+ const stat = yield* fs.stat(context.realCommandPath);
+ if (Number(stat.size) > 128 * 1_024) return false;
+ const source = yield* fs.readFileString(context.realCommandPath);
+ return source.includes("muse-code/launcher-") && source.includes("MUSE_SYNC_UPDATE");
+ }).pipe(Effect.orElseSucceed(() => false));
+ if (!isLauncher) return manual;
+ // The launcher has no `update` subcommand. Its documented launch environment
+ // forces a synchronous refresh of this executable's own installation.
+ const updateEnv = {
+ ...(context.env.MUSE_CHANNEL ? { MUSE_CHANNEL: context.env.MUSE_CHANNEL } : {}),
+ MUSE_NO_AUTO_UPDATE: "0",
+ MUSE_SYNC_UPDATE: "1",
+ MUSE_UPDATE_INTERVAL_SECONDS: "0",
+ };
+ return {
+ ...makeProviderMaintenanceCapabilities({
+ provider: DRIVER,
+ packageName: null,
+ updateExecutable: context.resolvedCommandPath,
+ updateArgs: ["--version"],
+ updateCommand: [
+ ...Object.entries(updateEnv).map(([key, value]) => `${key}=${shellQuote(value)}`),
+ shellQuote(context.resolvedCommandPath),
+ "--version",
+ ].join(" "),
+ updateLockKey: `muse:${context.realCommandPath}`,
+ platform: context.platform,
+ env: { ...context.env, ...updateEnv },
+ }),
+ compareVersions: compareMuseVersions,
+ };
+ }),
+};
+
+function shellQuote(word: string) {
+ return /^[\w./:@=-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
+}
+
+export const latestMuseVersion = Effect.fn("latestMuseVersion")(function* (
+ environment: NodeJS.ProcessEnv,
+ options?: { readonly fresh?: boolean },
+) {
+ const channel = environment.MUSE_CHANNEL || "muse-stable";
+ if (!MUSE_CHANNELS.has(channel)) return null;
+ const channelUrl = `https://api.meta.ai/muse-code/channels/${channel}`;
+ const cache = yield* ProviderVersionCache;
+ const key = `muse:${channelUrl}`;
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
+ const cached = cache.get(key);
+ if (!options?.fresh && cached && cached.expiresAt > now) return cached.version;
+ const client = yield* HttpClient.HttpClient;
+ const version = yield* client.execute(HttpClientRequest.get(channelUrl)).pipe(
+ Effect.flatMap((response) =>
+ response.status >= 200 && response.status < 300
+ ? response.json.pipe(
+ Effect.flatMap(Schema.decodeUnknownEffect(ChannelManifest)),
+ Effect.map((manifest) => parseMuseVersion(manifest.version)),
+ )
+ : Effect.succeed(null),
+ ),
+ Effect.timeout(4_000),
+ Effect.orElseSucceed(() => null),
+ );
+ cache.set(key, { version, expiresAt: now + (version === null ? 60_000 : 60 * 60 * 1_000) });
+ return version;
+});
+
+/** Feed Muse's release channel into the same advisory and notification flow as other drivers. */
+export const enrichMuseSnapshot = Effect.fn("enrichMuseSnapshot")(function* (input: {
+ readonly snapshot: ServerProvider;
+ readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
+ readonly enableProviderUpdateChecks: boolean;
+ readonly environment: NodeJS.ProcessEnv;
+}) {
+ const { snapshot } = input;
+ const latestVersion =
+ input.enableProviderUpdateChecks && snapshot.enabled && snapshot.installed && snapshot.version
+ ? yield* latestMuseVersion(input.environment)
+ : null;
+ return yield* enrichProviderSnapshotWithVersionAdvisory(
+ snapshot,
+ { ...input.maintenanceCapabilities, latestVersion },
+ { enableProviderUpdateChecks: input.enableProviderUpdateChecks },
+ );
+});
diff --git a/packages/provider-muse/src/server/modelCatalog.test.ts b/packages/provider-muse/src/server/modelCatalog.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..628628a161bd33320984cb8d2a789c7a2dba9398
--- /dev/null
+++ b/packages/provider-muse/src/server/modelCatalog.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, it } from "@effect/vitest";
+import { MuseSettings } from "../settings.ts";
+import * as Effect from "effect/Effect";
+import * as Schema from "effect/Schema";
+
+import type { MuseSdkHost } from "./sdk.ts";
+import {
+ discoverMuseModels,
+ museModelCapabilities,
+ resolveMuseReasoningEffort,
+} from "./modelCatalog.ts";
+
+const settings = Schema.decodeSync(MuseSettings)({});
+const host = (catalog: Record): MuseSdkHost => ({
+ connection: {
+ request: async () => catalog,
+ command: async () => ({}),
+ mintCommandId: () => "test",
+ onNotification: () => {},
+ onServerRequest: () => {},
+ onProtocolError: () => {},
+ closed: new Promise(() => {}),
+ },
+ initializeResult: { grantedCapabilities: [] },
+ exited: new Promise(() => {}),
+ close: async () => {},
+});
+
+const row = (modelId: string, extra: Record = {}) => ({
+ modelId,
+ displayLabel: modelId,
+ providerId: "meta",
+ profileId: "tbh",
+ isDefault: false,
+ contextLimit: 1_000_000,
+ ...extra,
+});
+
+describe("Muse model catalog", () => {
+ it.effect("uses the labels, efforts and defaults that model/list sends", () =>
+ Effect.gen(function* () {
+ const models = yield* discoverMuseModels(settings, {}, undefined, async () =>
+ host({
+ providerId: "meta",
+ profileId: "tbh",
+ source: "providerCatalog",
+ models: [
+ row("restful-walrus", {
+ displayLabel: " Restful Walrus ",
+ isDefault: true,
+ variants: ["minimal", "low", "high", "future-tier"],
+ reasoningEffortVariants: [{ tier: "low", description: "Quick" }],
+ defaultReasoningEffort: "high",
+ }),
+ row("older-host"),
+ row("unknown-efforts", { variants: "unknown" }),
+ row("foreign", { providerId: "another" }),
+ ],
+ }),
+ ).pipe(Effect.scoped);
+ expect(models.map(({ slug, name, isDefault }) => [slug, name, isDefault])).toEqual([
+ ["restful-walrus", "Restful Walrus", true],
+ ["older-host", "older-host", false],
+ ["unknown-efforts", "unknown-efforts", false],
+ ]);
+ expect(models[0]?.capabilities?.optionDescriptors).toEqual([
+ {
+ id: "reasoningEffort",
+ label: "Reasoning",
+ type: "select",
+ currentValue: "high",
+ options: [
+ { id: "minimal", label: "Minimal" },
+ { id: "low", label: "Low", description: "Quick" },
+ { id: "high", label: "High", isDefault: true },
+ ],
+ },
+ ]);
+ // Older hosts and "unknown" fall back to Muse's documented tiers.
+ expect(models[1]?.capabilities).toEqual(museModelCapabilities());
+ expect(models[2]?.capabilities).toEqual(museModelCapabilities());
+ }),
+ );
+
+ it("normalizes saved and implicit efforts against the selected model before dispatch", () => {
+ const fallback = museModelCapabilities();
+ expect(resolveMuseReasoningEffort(fallback, undefined)).toBe("high");
+ expect(resolveMuseReasoningEffort(fallback, "max")).toBe("max");
+ expect(resolveMuseReasoningEffort(fallback, "ultra")).toBe("high");
+ const restricted = museModelCapabilities({ variants: ["xhigh"] });
+ expect(resolveMuseReasoningEffort(restricted, undefined)).toBe("xhigh");
+ expect(resolveMuseReasoningEffort(restricted, "low")).toBe("xhigh");
+ expect(
+ resolveMuseReasoningEffort(museModelCapabilities({ variants: [] }), "max"),
+ ).toBeUndefined();
+ expect(resolveMuseReasoningEffort(undefined, "max")).toBe("max");
+ });
+});
diff --git a/packages/provider-muse/src/server/modelCatalog.ts b/packages/provider-muse/src/server/modelCatalog.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f390d062803c9ab81892d23856aa11770eea5a8c
--- /dev/null
+++ b/packages/provider-muse/src/server/modelCatalog.ts
@@ -0,0 +1,139 @@
+import type { SendUserTurnOptions } from "@muse-code/sdk";
+import { ModelCapabilities, ServerProviderModel } from "@t3tools/contracts";
+import { MuseSettings } from "../settings.ts";
+import { createModelCapabilities, getProviderOptionDescriptors } from "@t3tools/shared/model";
+import * as Effect from "effect/Effect";
+import * as Schema from "effect/Schema";
+import { createMuseSdkHost, createMuseSdkHostEffect, type MuseSdkHost } from "./sdk.ts";
+
+type MuseReasoningEffort = NonNullable["reasoningEffort"]>;
+
+const REASONING_EFFORT_LABELS = {
+ none: "None",
+ minimal: "Minimal",
+ low: "Low",
+ medium: "Medium",
+ high: "High",
+ xhigh: "Extra High",
+ max: "Max",
+ ultra: "Ultra",
+} satisfies Record;
+
+const isMuseReasoningEffort = (tier: string): tier is MuseReasoningEffort =>
+ Object.hasOwn(REASONING_EFFORT_LABELS, tier);
+
+/** Used when Muse does not report a model's efforts (custom slugs, older hosts, "unknown"). */
+const FALLBACK_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
+/** Muse's own CLI default. */
+const FALLBACK_DEFAULT_EFFORT = "high";
+
+const ModelCatalogEntry = Schema.Struct({
+ modelId: Schema.NonEmptyString,
+ displayLabel: Schema.String,
+ providerId: Schema.String,
+ isDefault: Schema.Boolean,
+ variants: Schema.optional(Schema.Union([Schema.Array(Schema.String), Schema.Literal("unknown")])),
+ defaultReasoningEffort: Schema.optional(Schema.String),
+ reasoningEffortVariants: Schema.optional(
+ Schema.Array(
+ Schema.Struct({ tier: Schema.String, description: Schema.optional(Schema.String) }),
+ ),
+ ),
+});
+const decodeModelList = Schema.decodeUnknownEffect(
+ Schema.Struct({ providerId: Schema.String, models: Schema.Array(ModelCatalogEntry) }),
+);
+
+/** Builds the reasoning picker from a `model/list` row, or the fallback when called without one. */
+export function museModelCapabilities(
+ model?: Pick<
+ typeof ModelCatalogEntry.Type,
+ "variants" | "defaultReasoningEffort" | "reasoningEffortVariants"
+ >,
+): ModelCapabilities {
+ const reported = model?.variants !== undefined && model.variants !== "unknown";
+ const efforts = (reported ? model.variants : FALLBACK_EFFORTS).filter(isMuseReasoningEffort);
+ const preferredDefault = reported ? model?.defaultReasoningEffort : FALLBACK_DEFAULT_EFFORT;
+ const defaultValue = efforts.find((tier) => tier === preferredDefault) ?? efforts[0];
+ if (!defaultValue) return createModelCapabilities({ optionDescriptors: [] });
+ return createModelCapabilities({
+ optionDescriptors: [
+ {
+ id: "reasoningEffort",
+ label: "Reasoning",
+ type: "select",
+ currentValue: defaultValue,
+ options: efforts.map((id) => {
+ const description = model?.reasoningEffortVariants?.find(
+ (variant) => variant.tier === id,
+ )?.description;
+ return {
+ id,
+ label: REASONING_EFFORT_LABELS[id],
+ ...(id === defaultValue ? { isDefault: true } : {}),
+ ...(description ? { description } : {}),
+ };
+ }),
+ },
+ ],
+ });
+}
+
+/** Apply the advertised model choices to saved, remembered and implicit efforts at dispatch. */
+export function resolveMuseReasoningEffort(
+ capabilities: ModelCapabilities | null | undefined,
+ effort: string | undefined,
+): string | undefined {
+ if (!capabilities) return effort;
+ const descriptor = getProviderOptionDescriptors({
+ caps: capabilities,
+ selections: effort ? [{ id: "reasoningEffort", value: effort }] : undefined,
+ }).find((descriptor) => descriptor.id === "reasoningEffort");
+ if (descriptor?.type !== "select") return undefined;
+ return descriptor.options.find((option) => option.id === descriptor.currentValue)?.id;
+}
+
+class MuseCatalogError extends Schema.TaggedError()("MuseCatalogError", {
+ detail: Schema.String,
+}) {}
+
+export const discoverMuseModels = Effect.fn("discoverMuseModels")(function* (
+ settings: MuseSettings,
+ environment: NodeJS.ProcessEnv | undefined,
+ cwd?: string,
+ createHost: typeof createMuseSdkHost = createMuseSdkHost,
+) {
+ const host = yield* Effect.acquireRelease(
+ createMuseSdkHostEffect(
+ {
+ binaryPath: settings.binaryPath,
+ ...(environment ? { environment } : {}),
+ ...(cwd ? { cwd } : {}),
+ readOnly: true,
+ startupTimeoutMs: 8_000,
+ },
+ createHost,
+ ),
+ (host: MuseSdkHost) => Effect.promise(() => host.close()),
+ { interruptible: true },
+ );
+ const result = yield* Effect.tryPromise(() => host.connection.request("model/list", {}));
+ const catalog = yield* decodeModelList(result);
+ if (catalog.providerId !== "meta") {
+ return yield* new MuseCatalogError({ detail: "Muse returned a catalog for another provider." });
+ }
+ const seen = new Set();
+ return catalog.models.flatMap((model): ServerProviderModel[] => {
+ if (model.providerId !== "meta" || seen.has(model.modelId)) return [];
+ seen.add(model.modelId);
+ return [
+ {
+ slug: model.modelId,
+ name: model.displayLabel.trim() || model.modelId,
+ isCustom: false,
+ isDefault: model.isDefault,
+ capabilities: museModelCapabilities(model),
+ },
+ ];
+ });
+});
diff --git a/packages/provider-muse/src/server/protocol.test.ts b/packages/provider-muse/src/server/protocol.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d583d84de4953709c56e0b80f858246e5bda94b5
--- /dev/null
+++ b/packages/provider-muse/src/server/protocol.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vite-plus/test";
+
+import { museApprovalChoices } from "./protocol.ts";
+
+// Muse 1.4.3 shell approvals offer only `abort`; the muse_permission replay covers that case.
+describe("Muse approval choices", () => {
+ it("keeps denied and abort separate when Muse offers both", () => {
+ const choices = museApprovalChoices({
+ availableChoices: [
+ { choiceId: "deny", label: "Deny", decision: "denied", scope: "once" },
+ { choiceId: "abort", label: "Stop", decision: "abort", scope: "once" },
+ ],
+ });
+ expect(choices.get("decline")?.choiceId).toBe("deny");
+ expect(choices.get("cancel")?.choiceId).toBe("abort");
+ });
+});
diff --git a/packages/provider-muse/src/server/protocol.ts b/packages/provider-muse/src/server/protocol.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91befa21e0485d2e3ed2d5125b9d03c0ae7498db
--- /dev/null
+++ b/packages/provider-muse/src/server/protocol.ts
@@ -0,0 +1,228 @@
+import type { ProviderApprovalDecision } from "@t3tools/contracts";
+import * as Schema from "effect/Schema";
+
+const NonEmptyString = Schema.String.check(Schema.isMinLength(1));
+
+const MuseUsage = Schema.Struct({
+ inputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ outputTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ cachedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ reasoningTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ cacheReadTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
+ cacheWriteTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
+});
+
+export const MuseItem = Schema.Struct({
+ /** `workflow`: the launched entry's name. */
+ entryId: Schema.optional(Schema.String),
+ /** `workflow`: each child agent's latest attempt, re-sent whole on every change. */
+ children: Schema.optional(
+ Schema.Array(
+ Schema.Struct({
+ childId: NonEmptyString,
+ attempt: Schema.Int,
+ status: Schema.String,
+ label: Schema.optional(Schema.String),
+ terminal: Schema.optional(Schema.String),
+ durationMs: Schema.optional(Schema.Finite),
+ failureReason: Schema.optional(Schema.String),
+ }),
+ ),
+ ),
+ itemId: NonEmptyString,
+ kind: NonEmptyString,
+ revision: Schema.Int.check(Schema.isGreaterThan(0)),
+ status: NonEmptyString,
+ turnId: Schema.optional(Schema.NullOr(Schema.String)),
+ text: Schema.optional(Schema.String),
+ summary: Schema.optional(Schema.Array(Schema.String)),
+ tool: Schema.optional(Schema.String),
+ args: Schema.optional(Schema.String),
+ visibleOutput: Schema.optional(Schema.String),
+ truncated: Schema.optional(Schema.Boolean),
+ failureReason: Schema.optional(Schema.String),
+ fallbackText: Schema.optional(Schema.String),
+ commandText: Schema.optional(Schema.String),
+ exitCode: Schema.optional(Schema.Int),
+ durationMs: Schema.optional(Schema.Finite),
+ subagentId: Schema.optional(Schema.String),
+ childSessionId: Schema.optional(Schema.String),
+ objective: Schema.optional(Schema.String),
+ role: Schema.optional(Schema.String),
+ controlStatus: Schema.optional(Schema.String),
+ result: Schema.optional(
+ Schema.Struct({ summary: Schema.String, text: Schema.optional(Schema.String) }),
+ ),
+ usage: Schema.optional(MuseUsage),
+ outcome: Schema.optional(Schema.String),
+ trigger: Schema.optional(Schema.String),
+ reason: Schema.optional(Schema.String),
+});
+export type MuseItem = typeof MuseItem.Type;
+
+export const MuseSessionResult = Schema.Struct({
+ session: Schema.Struct({
+ sessionId: NonEmptyString,
+ modelId: Schema.optional(Schema.NullOr(Schema.String)),
+ activeTurnId: Schema.optional(Schema.NullOr(Schema.String)),
+ }),
+ history: Schema.optional(
+ Schema.Struct({
+ items: Schema.NullOr(Schema.Array(MuseItem)),
+ snapshot: Schema.optional(
+ Schema.NullOr(
+ Schema.Struct({
+ schemaVersion: Schema.optional(Schema.Int),
+ state: Schema.Struct({ items: Schema.Array(MuseItem) }),
+ }),
+ ),
+ ),
+ }),
+ ),
+});
+export const MuseTurnStartResult = Schema.Struct({ turnId: NonEmptyString });
+
+export const MuseApproval = Schema.Struct({
+ approvalId: NonEmptyString,
+ protectedWrite: Schema.optional(Schema.Boolean),
+ judgeEscalated: Schema.optional(Schema.Boolean),
+ sessionId: NonEmptyString,
+ turnId: Schema.optional(Schema.String),
+ itemId: Schema.optional(Schema.String),
+ toolName: Schema.optional(Schema.String),
+ rawArgs: Schema.optional(Schema.String),
+ /** Set when a workflow child asks; `turnId` is then the child's run, not a session turn. */
+ subagentOrigin: Schema.optional(Schema.Struct({ subagentId: NonEmptyString })),
+ currentRequirementId: Schema.Struct({ approvalId: NonEmptyString, sourceIndex: Schema.Int }),
+ availableChoices: Schema.Array(
+ Schema.Struct({
+ choiceId: NonEmptyString,
+ label: Schema.String,
+ decision: NonEmptyString,
+ scope: Schema.String,
+ }),
+ ),
+ subject: Schema.Struct({
+ kind: Schema.String,
+ access: Schema.optional(Schema.String),
+ command: Schema.optional(Schema.String),
+ path: Schema.optional(Schema.String),
+ host: Schema.optional(Schema.String),
+ target: Schema.optional(Schema.String),
+ }),
+});
+export type MuseApproval = typeof MuseApproval.Type;
+
+export const MuseUserInput = Schema.Struct({
+ userInputId: NonEmptyString,
+ sessionId: NonEmptyString,
+ turnId: NonEmptyString,
+ questions: Schema.Array(
+ Schema.Struct({
+ id: NonEmptyString,
+ header: Schema.String,
+ question: Schema.String,
+ options: Schema.Array(
+ Schema.Struct({ label: Schema.String, description: Schema.optional(Schema.String) }),
+ ),
+ selection: Schema.Struct({
+ mode: Schema.Literals(["single", "multiple"]),
+ minSelections: Schema.optional(Schema.Int),
+ maxSelections: Schema.optional(Schema.Int),
+ }),
+ }),
+ ),
+});
+export type MuseUserInput = typeof MuseUserInput.Type;
+
+export const MuseTokenUsageEvent = Schema.Struct({
+ turnId: NonEmptyString,
+ promptTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ usage: MuseUsage,
+});
+export const MuseContextUsage = Schema.Struct({
+ usedTokens: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
+ windowTokens: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
+});
+export const MuseTodoList = Schema.Struct({
+ items: Schema.Array(Schema.Struct({ text: Schema.String, status: Schema.String })),
+});
+export const MuseTurnCompleted = Schema.Struct({
+ turnId: NonEmptyString,
+ terminal: NonEmptyString,
+ reason: Schema.optional(Schema.String),
+ error: Schema.optional(Schema.Struct({ message: Schema.String })),
+ usage: Schema.optional(MuseUsage),
+});
+export const MuseTurnRetryScheduled = Schema.Struct({
+ turnId: NonEmptyString,
+ nextAttempt: Schema.Int.check(Schema.isGreaterThan(0)),
+ maxAttempts: Schema.Int.check(Schema.isGreaterThan(0)),
+ reason: NonEmptyString,
+ retryDelayMs: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
+});
+export const MuseCompactResult = Schema.Struct({
+ status: Schema.String,
+ reason: Schema.optional(Schema.String),
+});
+
+export const MuseItemEvent = Schema.Struct({ item: MuseItem });
+export const MuseDelta = Schema.Struct({
+ itemId: NonEmptyString,
+ delta: Schema.String,
+ field: Schema.optional(Schema.String),
+});
+
+/** The T3 decision for one native Muse choice, or none when T3 has no equivalent. */
+export function museApprovalDecision(
+ choice: Pick,
+): ProviderApprovalDecision | undefined {
+ switch (choice.decision) {
+ case "approved":
+ return "accept";
+ case "approvedForSession":
+ return "acceptForSession";
+ case "approvedPolicyAmendment":
+ return choice.scope === "session"
+ ? "acceptForSession"
+ : choice.scope === "localPersistent"
+ ? "acceptAlways"
+ : undefined;
+ case "denied":
+ case "deniedPolicyAmendment":
+ return "decline";
+ case "abort":
+ return "cancel";
+ default:
+ return undefined;
+ }
+}
+
+/**
+ * Maps each canonical decision to one native choice. Muse shell approvals offer
+ * no `denied` choice, only `abort` ("Reject"), so `decline` falls back to it.
+ */
+export function museApprovalChoices(approval: Pick) {
+ const choices = new Map();
+ const rank = (choice: MuseApproval["availableChoices"][number]) =>
+ (choice.scope === "once" ? 0 : choice.scope === "session" ? 2 : 4) +
+ (choice.decision.endsWith("PolicyAmendment") ? 1 : 0);
+ for (const choice of approval.availableChoices) {
+ const decision = museApprovalDecision(choice);
+ if (!decision) continue;
+ const previous = choices.get(decision);
+ if (!previous || rank(choice) < rank(previous)) choices.set(decision, choice);
+ }
+ const abort = choices.get("cancel");
+ if (!choices.has("decline") && abort) choices.set("decline", abort);
+ return choices;
+}
+
+/** The approval buttons to show: one per native choice, preferring `decline` over `cancel`. */
+export function museApprovalOptions(approval: Pick) {
+ const choices = museApprovalChoices(approval);
+ const declineChoiceId = choices.get("decline")?.choiceId;
+ return [...choices]
+ .filter(([decision, choice]) => decision !== "cancel" || choice.choiceId !== declineChoiceId)
+ .map(([decision, choice]) => ({ decision, label: choice.label }));
+}
diff --git a/packages/provider-muse/src/server/sdk.test.ts b/packages/provider-muse/src/server/sdk.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c92498ca5170f10839e2e65e3e624ad75e3eb6f
--- /dev/null
+++ b/packages/provider-muse/src/server/sdk.test.ts
@@ -0,0 +1,266 @@
+import { spawnMspConnection, type MspHandshake, type SpawnedMspConnection } from "@muse-code/sdk";
+import { it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import { afterEach, describe, expect, vi } from "vite-plus/test";
+import {
+ createMuseSdkHost,
+ createMuseSdkHostEffect,
+ makeMuseEnvironment,
+ museApprovalMode,
+ type MuseSdkHost,
+} from "./sdk.ts";
+
+vi.mock("@muse-code/sdk", () => ({ spawnMspConnection: vi.fn() }));
+
+function pending() {
+ let resolve = (_value: A) => {};
+ let reject = (_error: unknown) => {};
+ const promise = new Promise((yes, no) => {
+ resolve = yes;
+ reject = no;
+ });
+ return { promise, resolve, reject };
+}
+
+function mockSpawn() {
+ const startup = pending();
+ const shutdown = pending<{ code: number; signal: null }>();
+ const closing = pending();
+ const initializing = pending();
+ const initialize = vi.fn(() => {
+ initializing.resolve();
+ return startup.promise;
+ });
+ const close = vi.fn(() => {
+ closing.resolve();
+ return shutdown.promise;
+ });
+ const handshake = { initialize, close } as unknown as MspHandshake;
+ vi.mocked(spawnMspConnection).mockReturnValue(handshake);
+ const ready = {
+ connection: {},
+ initializeResult: { grantedCapabilities: [], schema: { version: 1 } },
+ exited: shutdown.promise,
+ fingerprintWarning: { warning: "additive optional" },
+ } as unknown as SpawnedMspConnection;
+ return { startup, shutdown, closing, initializing, initialize, close, ready };
+}
+
+afterEach(() => {
+ vi.useRealTimers();
+ vi.clearAllMocks();
+});
+
+describe("Muse SDK host", () => {
+ it.each(["META_API_KEY", "meta_api_key", "Meta_Api_Key"])(
+ "excludes %s without reading its value or mutating the supplied environment",
+ (key) => {
+ const source: NodeJS.ProcessEnv = { PATH: "/bin", MUSE_TEST: "yes" };
+ const read = vi.fn(() => {
+ throw new Error("The API override must not be read.");
+ });
+ Object.defineProperty(source, key, { enumerable: true, get: read });
+ expect(makeMuseEnvironment(source)).toEqual({
+ PATH: "/bin",
+ MUSE_TEST: "yes",
+ MUSE_NO_AUTO_UPDATE: "1",
+ });
+ expect(read).not.toHaveBeenCalled();
+ expect(Object.keys(source)).toContain(key);
+ },
+ );
+
+ it("selects full access explicitly and keeps other runtime modes sandboxed", async () => {
+ for (const mode of ["approval-required", "auto-accept-edits", "auto", "full-access"] as const) {
+ const fake = mockSpawn();
+ const running = createMuseSdkHost({
+ binaryPath: "/custom/muse",
+ cwd: "/workspace",
+ runtimeMode: mode,
+ environment: { PATH: "/bin" },
+ });
+ expect(vi.mocked(spawnMspConnection).mock.lastCall?.[0]).toMatchObject({
+ command: "/custom/muse",
+ cwd: "/workspace",
+ args:
+ mode === "full-access"
+ ? ["serve", "--trust-workspace", "--disable-sandbox"]
+ : ["serve", "--trust-workspace"],
+ });
+ expect(museApprovalMode(mode)).toBe(mode === "full-access" ? "allowAll" : "promptUnmatched");
+ fake.startup.resolve(fake.ready);
+ const host = await running;
+ fake.shutdown.resolve({ code: 0, signal: null });
+ await host.close();
+ await host.close();
+ expect(fake.close).toHaveBeenCalledOnce();
+ }
+ });
+
+ it("bounds startup and waits for native shutdown after timeout", async () => {
+ vi.useFakeTimers();
+ const fake = mockSpawn();
+ const startup = createMuseSdkHost({ binaryPath: "muse", startupTimeoutMs: 30 });
+ let settled = false;
+ const outcome = startup.then(
+ () => {
+ settled = true;
+ },
+ (error: unknown) => {
+ settled = true;
+ return error;
+ },
+ );
+ await vi.advanceTimersByTimeAsync(31);
+ expect(fake.close).toHaveBeenCalledOnce();
+ expect(settled).toBe(false);
+ fake.shutdown.resolve({ code: 0, signal: null });
+ expect(await outcome).toMatchObject({ message: "Muse SDK initialization timed out." });
+ });
+
+ it("closes the pre-handshake host on abort and rejects only after shutdown", async () => {
+ const fake = mockSpawn();
+ const abort = new AbortController();
+ const started = createMuseSdkHost({ binaryPath: "muse", signal: abort.signal });
+ const reason = new Error("Cancelled by caller");
+ const outcome = started.catch((error: unknown) => error);
+ abort.abort(reason);
+ expect(fake.close).toHaveBeenCalledOnce();
+ fake.shutdown.resolve({ code: 0, signal: null });
+ expect(await outcome).toBe(reason);
+ });
+
+ it("closes rejected initialization and keeps successful schema advisories usable", async () => {
+ const failed = mockSpawn();
+ const started = createMuseSdkHost({ binaryPath: "muse" });
+ const outcome = started.catch((error: unknown) => error);
+ failed.startup.reject(new Error("Handshake failed"));
+ failed.shutdown.resolve({ code: 0, signal: null });
+ expect(await outcome).toMatchObject({ message: "Handshake failed" });
+ expect(failed.close).toHaveBeenCalledOnce();
+ const success = mockSpawn();
+ const ready = createMuseSdkHost({ binaryPath: "muse", readOnly: true });
+ success.startup.resolve(success.ready);
+ const host = await ready;
+ expect(host.initializeResult.grantedCapabilities).toEqual([]);
+ expect(vi.mocked(spawnMspConnection).mock.lastCall?.[0].args).toEqual([
+ "serve",
+ "--disable-shell",
+ "--disable-write",
+ "--no-session-log",
+ ]);
+ success.shutdown.resolve({ code: 0, signal: null });
+ await host.close();
+ });
+
+ it.each([undefined, 2])(
+ "rejects unsupported envelope version %s and awaits shutdown",
+ async (version) => {
+ const fake = mockSpawn();
+ const started = createMuseSdkHost({ binaryPath: "muse" });
+ let settled = false;
+ const outcome = started.catch((error: unknown) => {
+ settled = true;
+ return error;
+ });
+ fake.startup.resolve({
+ ...fake.ready,
+ initializeResult: {
+ ...fake.ready.initializeResult,
+ schema: { fingerprint: "test", version },
+ },
+ } as unknown as SpawnedMspConnection);
+ await fake.closing.promise;
+ expect(fake.close).toHaveBeenCalledOnce();
+ expect(settled).toBe(false);
+ fake.shutdown.resolve({ code: 0, signal: null });
+ expect(await outcome).toMatchObject({
+ message: "Muse SDK returned an unsupported protocol envelope version.",
+ });
+ },
+ );
+
+ it("does not spawn when already aborted and closes a ready host on later abort", async () => {
+ const aborted = new AbortController();
+ aborted.abort();
+ await expect(
+ createMuseSdkHost({ binaryPath: "muse", signal: aborted.signal }),
+ ).rejects.toBeDefined();
+ expect(spawnMspConnection).not.toHaveBeenCalled();
+ const fake = mockSpawn();
+ const abort = new AbortController();
+ const running = createMuseSdkHost({ binaryPath: "muse", signal: abort.signal });
+ fake.startup.resolve(fake.ready);
+ const host = await running;
+ abort.abort();
+ fake.shutdown.resolve({ code: 0, signal: null });
+ await host.close();
+ expect(fake.close).toHaveBeenCalledOnce();
+ });
+
+ it.effect(
+ "keeps Effect startup resources alive until interrupted initialization shuts down",
+ () =>
+ Effect.gen(function* () {
+ const fake = mockSpawn();
+ let released = false;
+ const fiber = yield* Effect.forkChild(
+ createMuseSdkHostEffect({ binaryPath: "muse" }).pipe(
+ Effect.ensuring(
+ Effect.sync(() => {
+ released = true;
+ }),
+ ),
+ ),
+ );
+ yield* Effect.promise(() => fake.initializing.promise);
+ const interrupted = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild);
+ yield* Effect.promise(() => fake.closing.promise);
+ expect(released).toBe(false);
+ fake.shutdown.resolve({ code: 0, signal: null });
+ yield* Fiber.join(interrupted);
+ expect(released).toBe(true);
+ expect(fake.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("closes a fulfilled host when interruption wins before acquisition is delivered", () =>
+ Effect.gen(function* () {
+ const started = pending();
+ const startup = pending();
+ const closing = pending();
+ const shutdown = pending();
+ const host = {
+ close: vi.fn(() => {
+ closing.resolve();
+ return shutdown.promise;
+ }),
+ } as unknown as MuseSdkHost;
+ let released = false;
+ const fiber = yield* Effect.forkChild(
+ createMuseSdkHostEffect({ binaryPath: "muse" }, () => {
+ started.resolve();
+ return startup.promise;
+ }).pipe(
+ Effect.ensuring(
+ Effect.sync(() => {
+ released = true;
+ }),
+ ),
+ ),
+ );
+ yield* Effect.promise(() => started.promise);
+ startup.resolve(host);
+ const interrupted = yield* Fiber.interrupt(fiber).pipe(
+ Effect.forkChild({ startImmediately: true }),
+ );
+ yield* Effect.promise(() => closing.promise);
+ expect(released).toBe(false);
+ shutdown.resolve();
+ yield* Fiber.join(interrupted);
+ expect(released).toBe(true);
+ expect(host.close).toHaveBeenCalledOnce();
+ }),
+ );
+});
diff --git a/packages/provider-muse/src/server/sdk.ts b/packages/provider-muse/src/server/sdk.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dcc778d4f77973d25832451b7bcd47f6390342cb
--- /dev/null
+++ b/packages/provider-muse/src/server/sdk.ts
@@ -0,0 +1,163 @@
+// @effect-diagnostics globalTimers:off
+// The SDK owns process shutdown; native deadlines cover initialize before an Effect resource exists.
+import {
+ spawnMspConnection,
+ type Connection,
+ type ProcessExit,
+ type SpawnedMspConnection,
+} from "@muse-code/sdk";
+import type { RuntimeMode } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+
+export interface MuseSdkHost {
+ readonly connection: Pick<
+ Connection,
+ | "command"
+ | "request"
+ | "mintCommandId"
+ | "onNotification"
+ | "onServerRequest"
+ | "onProtocolError"
+ | "closed"
+ >;
+ readonly initializeResult: Pick;
+ readonly exited: Promise;
+ /** The last lines Muse wrote to stderr, for explaining an unexpected exit. */
+ readonly stderrTail?: () => ReadonlyArray;
+ readonly close: () => Promise;
+}
+
+export interface MuseSdkHostOptions {
+ readonly binaryPath: string;
+ readonly cwd?: string;
+ readonly environment?: NodeJS.ProcessEnv;
+ readonly runtimeMode?: RuntimeMode;
+ /** Disables shell and filesystem writes and workspace trust. */
+ readonly readOnly?: boolean;
+ /** Read-only generation needs durable logging for turn/item notifications (verified through 1.1.1). */
+ readonly sessionLogging?: boolean;
+ readonly signal?: AbortSignal;
+ readonly startupTimeoutMs?: number;
+}
+
+/** Preserve the host's CLI login while excluding the API-key override before copying values. */
+export function makeMuseEnvironment(
+ environment: NodeJS.ProcessEnv = process.env,
+): NodeJS.ProcessEnv {
+ return {
+ ...Object.fromEntries(
+ Object.keys(environment)
+ .filter((key) => key.toUpperCase() !== "META_API_KEY")
+ .map((key) => [key, environment[key]]),
+ ),
+ MUSE_NO_AUTO_UPDATE: "1",
+ };
+}
+
+export function museApprovalMode(runtimeMode: RuntimeMode) {
+ return runtimeMode === "full-access" ? "allowAll" : "promptUnmatched";
+}
+
+/** The `muse serve` arguments for a host: read-only hosts get no shell, writes, or workspace trust. */
+export function museServeArgs(
+ options: Pick,
+) {
+ const args = ["serve"];
+ if (options.readOnly) {
+ args.push("--disable-shell", "--disable-write");
+ if (options.sessionLogging !== true) args.push("--no-session-log");
+ } else {
+ args.push("--trust-workspace");
+ if (options.runtimeMode === "full-access") args.push("--disable-sandbox");
+ }
+ return args;
+}
+
+/** What T3 sends in MSP `initialize`; only full hosts ask for session MCP servers. */
+export function museInitializeParams(readOnly = false) {
+ return {
+ clientInfo: { name: "t3_code", title: "T3 Code", version: "1" },
+ capabilities: { requestedCapabilities: readOnly ? [] : ["sessionMcp"] },
+ };
+}
+
+/** One SDK-owned process. The pre-handshake handle owns cleanup even when initialize never replies. */
+export async function createMuseSdkHost(
+ options: MuseSdkHostOptions,
+ spawn: typeof spawnMspConnection = spawnMspConnection,
+): Promise {
+ options.signal?.throwIfAborted();
+ const handshake = spawn({
+ command: options.binaryPath,
+ args: museServeArgs(options),
+ ...(options.cwd ? { cwd: options.cwd } : {}),
+ // Callers pass an environment already built with makeMuseEnvironment.
+ env: options.environment ?? makeMuseEnvironment(),
+ // A healthy host exits at once; this only bounds a hung one on close.
+ shutdownTimeoutMs: 10_000,
+ });
+ let closePromise: Promise | undefined;
+ const close = () => {
+ options.signal?.removeEventListener("abort", onAbort);
+ return (closePromise ??= handshake.close().then(() => undefined));
+ };
+ let rejectStartup: (reason: unknown) => void = () => {};
+ const onAbort = () => {
+ rejectStartup(options.signal?.reason ?? new Error("Muse SDK startup aborted."));
+ void close().catch(() => {});
+ };
+ const interrupted = new Promise((_resolve, reject) => {
+ rejectStartup = reject;
+ });
+ options.signal?.addEventListener("abort", onAbort, { once: true });
+ if (options.signal?.aborted) onAbort();
+ const timer = setTimeout(
+ () => rejectStartup(new Error("Muse SDK initialization timed out.")),
+ options.startupTimeoutMs ?? 20_000,
+ );
+ timer.unref();
+ try {
+ const host = await Promise.race([
+ handshake.initialize(museInitializeParams(options.readOnly)),
+ interrupted,
+ ]);
+ if (host.initializeResult.schema?.version !== 1) {
+ throw new Error("Muse SDK returned an unsupported protocol envelope version.");
+ }
+ // The SDK's fingerprint warning permits additive optional schema changes.
+ // Successful initialization, not fingerprint identity, determines readiness.
+ return {
+ connection: host.connection,
+ initializeResult: host.initializeResult,
+ exited: host.exited,
+ stderrTail: () => handshake.child.stderrTail,
+ close,
+ };
+ } catch (error) {
+ await close();
+ throw error;
+ } finally {
+ clearTimeout(timer);
+ }
+}
+
+/** Cancellation waits for native shutdown before callers release startup resources. */
+export const createMuseSdkHostEffect = Effect.fn("createMuseSdkHostEffect")(function* (
+ options: Omit,
+ createHost: typeof createMuseSdkHost = createMuseSdkHost,
+) {
+ let startup: Promise | undefined;
+ return yield* Effect.tryPromise((signal) => {
+ startup = createHost({ ...options, signal });
+ return startup;
+ }).pipe(
+ Effect.onInterrupt(() =>
+ Effect.promise(async () => {
+ await startup?.then(
+ (host) => host.close(),
+ () => {},
+ );
+ }),
+ ),
+ );
+});
diff --git a/packages/provider-muse/src/server/status.test.ts b/packages/provider-muse/src/server/status.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f7031de0ad7b6b20fe061603edf0ac43bae0045
--- /dev/null
+++ b/packages/provider-muse/src/server/status.test.ts
@@ -0,0 +1,167 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { expect, it } from "@effect/vitest";
+import { MuseSettings } from "../settings.ts";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Schema from "effect/Schema";
+import { vi } from "vite-plus/test";
+
+import { writeFakeCli } from "@t3tools/provider-testing/fakeCli";
+import type { MuseSdkHost } from "./sdk.ts";
+import { COMPACT_SLASH_COMMAND } from "@t3tools/provider-core/server/snapshotProbe";
+import { checkMuseProviderStatus } from "./status.ts";
+
+const settings = Schema.decodeSync(MuseSettings);
+const makeHost = (catalog: Record) => {
+ const host: MuseSdkHost = {
+ initializeResult: { grantedCapabilities: [] },
+ connection: {
+ request: vi.fn(async () => catalog),
+ command: vi.fn(async () => ({})),
+ mintCommandId: () => "test-command",
+ onNotification: () => {},
+ onServerRequest: () => {},
+ onProtocolError: () => {},
+ closed: new Promise(() => {}),
+ },
+ exited: new Promise(() => {}),
+ close: vi.fn(async () => {}),
+ };
+ return host;
+};
+const metaCatalog = {
+ providerId: "meta",
+ models: [
+ {
+ modelId: "muse-discovered",
+ displayLabel: "Muse Discovered",
+ providerId: "meta",
+ isDefault: true,
+ },
+ {
+ modelId: "foreign-model",
+ displayLabel: "Another provider",
+ providerId: "another",
+ isDefault: false,
+ },
+ ],
+};
+
+const fakeCli = (source = 'process.stdout.write("muse 1.0.3-R2198.1\\n");') =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-status-test-" });
+ return writeFakeCli({ directory, name: "muse", source });
+ });
+
+it.layer(NodeServices.layer)("Muse status", (it) => {
+ it.effect("does not start a host when disabled", () =>
+ Effect.gen(function* () {
+ const createHost = vi.fn(async () => makeHost(metaCatalog));
+ const snapshot = yield* checkMuseProviderStatus(settings({}), {}, undefined, createHost);
+ expect(snapshot.status).toBe("disabled");
+ expect(createHost).not.toHaveBeenCalled();
+ }),
+ );
+
+ it.effect("gives host-local install and login guidance for a missing binary", () =>
+ Effect.gen(function* () {
+ const snapshot = yield* checkMuseProviderStatus(
+ settings({ enabled: true, binaryPath: "/definitely-missing/muse" }),
+ );
+ expect(snapshot.installed).toBe(false);
+ expect(snapshot.status).toBe("error");
+ expect(snapshot.message).toContain("muse login");
+ expect(snapshot.message).toContain("this T3 server host");
+ }),
+ );
+
+ it.effect("does not expose CLI stderr or start an SDK host after a failed version check", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const binaryPath = yield* fakeCli(
+ 'process.stderr.write("secret-test-value");process.exit(2);',
+ );
+ const createHost = vi.fn(async () => makeHost(metaCatalog));
+ const snapshot = yield* checkMuseProviderStatus(
+ settings({ enabled: true, binaryPath }),
+ undefined,
+ undefined,
+ createHost,
+ );
+ expect(snapshot.installed).toBe(true);
+ expect(snapshot.status).toBe("error");
+ expect(snapshot.message).not.toContain("secret-test-value");
+ expect(createHost).not.toHaveBeenCalled();
+ }),
+ ),
+ );
+
+ it.effect("discovers the Meta catalog without treating it as authentication proof", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const binaryPath = yield* fakeCli();
+ const host = makeHost(metaCatalog);
+ const snapshot = yield* checkMuseProviderStatus(
+ settings({ enabled: true, binaryPath, customModels: ["muse-custom"] }),
+ undefined,
+ undefined,
+ async () => host,
+ );
+ expect(snapshot.status).toBe("ready");
+ expect(snapshot.message).toBeUndefined();
+ expect(snapshot.slashCommands).toEqual([COMPACT_SLASH_COMMAND]);
+ expect(snapshot.auth).toEqual({ status: "unknown" });
+ expect(snapshot.version).toBe("1.0.3-R2198.1");
+ expect(snapshot.models.map((model) => model.slug)).toEqual([
+ "muse-discovered",
+ "muse-custom",
+ ]);
+ const descriptor = snapshot.models[0]?.capabilities?.optionDescriptors?.[0];
+ expect(
+ descriptor?.type === "select" && descriptor.options.map((option) => option.id),
+ ).toEqual(["low", "medium", "high", "xhigh", "max"]);
+ expect(host.connection.request).toHaveBeenCalledWith("model/list", {});
+ expect(host.connection.command).not.toHaveBeenCalled();
+ expect(host.close).toHaveBeenCalledOnce();
+ }),
+ ),
+ );
+
+ it.effect("closes the host after malformed metadata and reports unavailable", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const binaryPath = yield* fakeCli();
+ const host = makeHost({ providerId: "meta", models: [{ modelId: 1 }] });
+ const snapshot = yield* checkMuseProviderStatus(
+ settings({ enabled: true, binaryPath }),
+ undefined,
+ undefined,
+ async () => host,
+ );
+ expect(snapshot.status).toBe("error");
+ expect(snapshot.auth.status).toBe("unknown");
+ expect(snapshot.models).toEqual([]);
+ expect(host.close).toHaveBeenCalledOnce();
+ }),
+ ),
+ );
+
+ it.effect("keeps an empty discovered catalog empty instead of advertising a fallback model", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const binaryPath = yield* fakeCli();
+ const host = makeHost({ providerId: "meta", models: [] });
+ const snapshot = yield* checkMuseProviderStatus(
+ settings({ enabled: true, binaryPath }),
+ undefined,
+ undefined,
+ async () => host,
+ );
+ expect(snapshot.status).toBe("warning");
+ expect(snapshot.models).toEqual([]);
+ expect(host.close).toHaveBeenCalledOnce();
+ }),
+ ),
+ );
+});
diff --git a/packages/provider-muse/src/server/status.ts b/packages/provider-muse/src/server/status.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2cb4e5360ff2572ffe97755c5b39f326c6af2a91
--- /dev/null
+++ b/packages/provider-muse/src/server/status.ts
@@ -0,0 +1,154 @@
+import { ServerProviderModel } from "@t3tools/contracts";
+import { MuseSettings } from "../settings.ts";
+import { resolveSpawnCommand } from "@t3tools/shared/shell";
+import * as DateTime from "effect/DateTime";
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import * as Result from "effect/Result";
+import { ChildProcess } from "effect/process";
+
+import { createMuseSdkHost, makeMuseEnvironment } from "./sdk.ts";
+import { parseMuseVersion } from "./maintenance.ts";
+import { discoverMuseModels, museModelCapabilities } from "./modelCatalog.ts";
+import {
+ buildServerProvider,
+ COMPACT_SLASH_COMMAND,
+ DEFAULT_TIMEOUT_MS,
+ isCommandMissingCause,
+ providerModelsFromSettings,
+ spawnAndCollect,
+ type ProviderProbeResult,
+} from "@t3tools/provider-core/server/snapshotProbe";
+
+const MUSE_PRESENTATION = {
+ displayName: "Muse Code",
+ showInteractionModeToggle: false,
+ reportsContextWindow: true,
+ // Muse has no native "accept edits" or reviewer-backed mode, so T3 offers only
+ // the two it maps directly: promptUnmatched and allowAll.
+ supportedRuntimeModes: ["approval-required", "full-access"],
+} as const;
+
+const FALLBACK_CAPABILITIES = museModelCapabilities();
+
+export const makePendingMuseProvider = Effect.fn("makePendingMuseProvider")(function* (
+ settings: MuseSettings,
+) {
+ return buildServerProvider({
+ presentation: MUSE_PRESENTATION,
+ enabled: settings.enabled,
+ checkedAt: DateTime.formatIso(yield* DateTime.now),
+ models: providerModelsFromSettings([], settings.customModels, FALLBACK_CAPABILITIES),
+ slashCommands: settings.enabled ? [COMPACT_SLASH_COMMAND] : [],
+ probe: {
+ installed: false,
+ version: null,
+ status: "warning",
+ auth: { status: "unknown" },
+ message: settings.enabled
+ ? "Checking Muse Code CLI availability..."
+ : "Muse Code is disabled in T3 Code settings.",
+ },
+ });
+});
+
+export const checkMuseProviderStatus = Effect.fn("checkMuseProviderStatus")(function* (
+ settings: MuseSettings,
+ environment?: NodeJS.ProcessEnv,
+ cwd?: string,
+ createHost: typeof createMuseSdkHost = createMuseSdkHost,
+) {
+ if (!settings.enabled) return yield* makePendingMuseProvider(settings);
+ const checkedAt = DateTime.formatIso(yield* DateTime.now);
+ // The driver passes a prepared environment; direct callers get the host's without META_API_KEY.
+ const museEnvironment = environment ?? makeMuseEnvironment();
+ const snapshot = (probe: ProviderProbeResult, models: ReadonlyArray = []) =>
+ buildServerProvider({
+ presentation: MUSE_PRESENTATION,
+ enabled: true,
+ checkedAt,
+ models: providerModelsFromSettings(models, settings.customModels, FALLBACK_CAPABILITIES),
+ slashCommands: [COMPACT_SLASH_COMMAND],
+ probe,
+ });
+ const versionResult = yield* Effect.gen(function* () {
+ const spawnCommand = yield* resolveSpawnCommand(settings.binaryPath, ["--version"], {
+ env: museEnvironment,
+ });
+ return yield* spawnAndCollect(
+ settings.binaryPath,
+ ChildProcess.make(spawnCommand.command, spawnCommand.args, {
+ env: museEnvironment,
+ shell: spawnCommand.shell,
+ ...(cwd ? { cwd } : {}),
+ }),
+ );
+ }).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
+
+ if (Result.isFailure(versionResult)) {
+ const missing = isCommandMissingCause(versionResult.failure);
+ return snapshot({
+ installed: !missing,
+ version: null,
+ status: "error",
+ auth: { status: "unknown" },
+ message: missing
+ ? "Muse Code CLI (`muse`) was not found. Install Muse Code and run `muse login` on this T3 server host."
+ : "Failed to execute Muse Code CLI. Check its binary path on this T3 server host.",
+ });
+ }
+ if (Option.isNone(versionResult.success)) {
+ return snapshot({
+ installed: true,
+ version: null,
+ status: "error",
+ auth: { status: "unknown" },
+ message: "Muse Code CLI version check timed out.",
+ });
+ }
+ const versionOutput = versionResult.success.value;
+ const version = parseMuseVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`);
+ if (versionOutput.code !== 0) {
+ return snapshot({
+ installed: true,
+ version,
+ status: "error",
+ auth: { status: "unknown" },
+ message: "Muse Code CLI is installed but failed to run.",
+ });
+ }
+
+ const catalog = yield* discoverMuseModels(settings, museEnvironment, cwd, createHost).pipe(
+ Effect.scoped,
+ Effect.timeoutOption(12_000),
+ Effect.result,
+ );
+ if (Result.isFailure(catalog) || Option.isNone(catalog.success)) {
+ return snapshot({
+ installed: true,
+ version,
+ status: "error",
+ auth: { status: "unknown" },
+ message:
+ "Muse Code SDK could not read the model catalog. Check your Muse installation and run `muse login` on this T3 server host.",
+ });
+ }
+ const models = catalog.success.value;
+ // MSP has no auth signal, and model/list answers from Muse's cache even when logged out.
+ // Auth failures surface on the first turn instead.
+ return snapshot(
+ {
+ installed: true,
+ version,
+ auth: { status: "unknown" },
+ ...(models.length > 0
+ ? { status: "ready" }
+ : {
+ status: "warning",
+ message:
+ "Muse Code returned no models. Run `muse login` on this T3 server host and refresh its status.",
+ }),
+ },
+ models,
+ );
+});
diff --git a/packages/provider-muse/src/server/textGeneration.test.ts b/packages/provider-muse/src/server/textGeneration.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..41e84dae23ecc6e64bca809ccfcf5b36d5b85419
--- /dev/null
+++ b/packages/provider-muse/src/server/textGeneration.test.ts
@@ -0,0 +1,372 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { expect, it } from "@effect/vitest";
+import type { NotificationHandler } from "@muse-code/sdk";
+import {
+ MUSE_DEFAULT_MODEL,
+ ProviderInstanceId,
+ type ServerProviderModel,
+} from "@t3tools/contracts";
+import { MuseSettings } from "../settings.ts";
+import { createModelSelection } from "@t3tools/shared/model";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Schema from "effect/Schema";
+import { vi } from "vite-plus/test";
+
+import type { MuseSdkHost } from "./sdk.ts";
+import { museModelCapabilities } from "./modelCatalog.ts";
+import { makeMuseTextGeneration } from "./textGeneration.ts";
+
+const settings = Schema.decodeSync(MuseSettings)({ enabled: true });
+const modelSelection = createModelSelection(
+ ProviderInstanceId.make("muse"),
+ "muse-spark-1.3-contributor",
+ [{ id: "reasoningEffort", value: "high" }],
+);
+const titleInput = {
+ cwd: "/project/should-not-be-used",
+ message: "Fix the login form",
+ modelSelection,
+};
+
+function fixture(
+ onTurn?: (emit: (method: string, params: Record) => void) => void,
+ acknowledgeTurn = true,
+ modelCatalog?: Effect.Effect>,
+) {
+ let handler: NotificationHandler = () => {};
+ let commandSequence = 0;
+ let activeTurnId: string | undefined;
+ const mintCommandId = () => `turn-${++commandSequence}`;
+ const started = Promise.withResolvers();
+ const closed = Promise.withResolvers();
+ const emit = (method: string, params: Record) =>
+ handler({
+ jsonrpc: "2.0",
+ method,
+ params: {
+ sessionId: "session-1",
+ turnId: activeTurnId,
+ ...params,
+ ...(params.item
+ ? {
+ item: {
+ revision: 1,
+ turnId: activeTurnId,
+ ...(params.item as Record),
+ },
+ }
+ : {}),
+ },
+ });
+ const host: MuseSdkHost = {
+ initializeResult: { grantedCapabilities: [] },
+ connection: {
+ command: vi.fn(async (method, _params, options) => {
+ // Connection.command overwrites params.commandId unless supplied in options.
+ const commandId = options?.commandId ?? mintCommandId();
+ if (method === "session/start") return { session: { sessionId: "session-1" } };
+ if (method === "turn/start") {
+ activeTurnId = commandId;
+ started.resolve();
+ onTurn?.(emit);
+ if (!acknowledgeTurn) return await new Promise>(() => {});
+ return { turnId: commandId };
+ }
+ return {};
+ }),
+ request: vi.fn(async () => ({})),
+ mintCommandId,
+ onNotification: (nextHandler) => {
+ handler = nextHandler;
+ },
+ onProtocolError: () => {},
+ onServerRequest: () => {},
+ closed: closed.promise,
+ },
+ close: vi.fn(async () => {
+ closed.resolve();
+ }),
+ exited: new Promise(() => {}),
+ };
+ const createHost = vi.fn(async () => host);
+ const make = makeMuseTextGeneration(settings, {
+ createHost,
+ ...(modelCatalog ? { modelCatalog } : {}),
+ });
+ return {
+ host,
+ createHost,
+ started: started.promise,
+ closeConnection: closed.resolve,
+ emit,
+ make,
+ };
+}
+
+const finish =
+ (text: string) => (emit: (method: string, params: Record) => void) => {
+ emit("item/updated", { item: { itemId: "answer", kind: "agentMessage", text: "partial" } });
+ emit("item/completed", {
+ item: { itemId: "answer", kind: "agentMessage", revision: 2, text },
+ });
+ emit("turn/completed", { terminal: "completed" });
+ };
+
+it.layer(NodeServices.layer)("Muse text generation", (it) => {
+ it.effect("validates the final item once and closes an isolated restrictive host", () =>
+ Effect.gen(function* () {
+ const test = fixture(finish('{"title":"Fix login form"}'));
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput)).toEqual({
+ title: "Fix login form",
+ });
+ expect(test.createHost).toHaveBeenCalledWith(
+ expect.objectContaining({ readOnly: true, sessionLogging: true }),
+ );
+ expect(test.createHost).not.toHaveBeenCalledWith(
+ expect.objectContaining({ cwd: titleInput.cwd }),
+ );
+ expect(test.host.connection.command).toHaveBeenCalledWith(
+ "session/start",
+ expect.objectContaining({
+ providerId: "meta",
+ modelId: modelSelection.model,
+ approvalMode: "denyUnmatched",
+ }),
+ );
+ expect(test.host.connection.command).toHaveBeenCalledWith(
+ "turn/start",
+ expect.objectContaining({
+ reasoningEffort: "high",
+ }),
+ { commandId: "turn-1" },
+ );
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("uses low effort by default and normalizes stale efforts against the model", () =>
+ Effect.gen(function* () {
+ for (const { saved, variants, expected } of [
+ { saved: undefined, variants: undefined, expected: "low" },
+ { saved: undefined, variants: ["medium", "xhigh"], expected: "medium" },
+ { saved: "max", variants: ["minimal", "max"], expected: "max" },
+ { saved: "ultra", variants: ["medium", "xhigh"], expected: "medium" },
+ { saved: "high", variants: [], expected: undefined },
+ ]) {
+ const test = fixture(
+ finish('{"title":"Fix login form"}'),
+ true,
+ Effect.succeed([
+ {
+ slug: modelSelection.model,
+ name: modelSelection.model,
+ isCustom: false,
+ capabilities: museModelCapabilities(variants ? { variants } : undefined),
+ },
+ ]),
+ );
+ const service = yield* test.make;
+ yield* service.generateThreadTitle({
+ ...titleInput,
+ modelSelection: createModelSelection(
+ ProviderInstanceId.make("muse"),
+ modelSelection.model,
+ saved !== undefined ? [{ id: "reasoningEffort", value: saved }] : undefined,
+ ),
+ });
+ const turn = vi
+ .mocked(test.host.connection.command)
+ .mock.calls.find(([method]) => method === "turn/start");
+ expect(turn?.[1].reasoningEffort).toBe(expected);
+ expect(Object.hasOwn(turn?.[1] ?? {}, "reasoningEffort")).toBe(expected !== undefined);
+ }
+ }),
+ );
+
+ it.effect("lets Muse choose the model when none is selected", () =>
+ Effect.gen(function* () {
+ for (const model of [MUSE_DEFAULT_MODEL, " "]) {
+ const test = fixture(finish('{"title":"Fix login form"}'));
+ const service = yield* test.make;
+ yield* service.generateThreadTitle({
+ ...titleInput,
+ modelSelection: createModelSelection(ProviderInstanceId.make("muse"), model),
+ });
+ const start = vi
+ .mocked(test.host.connection.command)
+ .mock.calls.find(([method]) => method === "session/start");
+ expect(start?.[1]).not.toHaveProperty("modelId");
+ }
+ }),
+ );
+
+ it.effect("rejects invalid structured output and releases the host", () =>
+ Effect.gen(function* () {
+ const test = fixture(finish('{"title":42}'));
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
+ _tag: "TextGenerationError",
+ detail: "Muse returned invalid structured output.",
+ });
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("ignores output and terminal notifications for another turn", () =>
+ Effect.gen(function* () {
+ const test = fixture((emit) => {
+ emit("item/completed", {
+ item: {
+ itemId: "other-answer",
+ kind: "agentMessage",
+ turnId: "another-turn",
+ text: '{"title":"Wrong title"}',
+ },
+ });
+ emit("turn/completed", { turnId: "another-turn", terminal: "completed" });
+ finish('{"title":"Fix login form"}')(emit);
+ });
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput)).toEqual({ title: "Fix login form" });
+ expect(test.host.connection.command).toHaveBeenCalledWith(
+ "turn/start",
+ expect.objectContaining({ sessionId: "session-1" }),
+ { commandId: "turn-1" },
+ );
+ }),
+ );
+
+ it.effect("rejects a truncated response even when its prefix contains valid JSON", () =>
+ Effect.gen(function* () {
+ const test = fixture((emit) => {
+ emit("item/completed", {
+ item: {
+ itemId: "answer",
+ kind: "agentMessage",
+ text: '{"title":"Incomplete answer"}',
+ truncated: true,
+ },
+ });
+ emit("turn/completed", { terminal: "completed" });
+ });
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
+ _tag: "TextGenerationError",
+ });
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("settles a completed turn even when its start acknowledgement never arrives", () =>
+ Effect.gen(function* () {
+ const test = fixture(finish('{"title":"Fix login form"}'), false);
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput)).toEqual({ title: "Fix login form" });
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("reports a failed turn without trying another provider", () =>
+ Effect.gen(function* () {
+ const test = fixture((emit) =>
+ emit("turn/completed", {
+ terminal: "failed",
+ error: { message: "Authentication required. Run muse login." },
+ }),
+ );
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
+ _tag: "TextGenerationError",
+ });
+ expect(test.createHost).toHaveBeenCalledOnce();
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("settles when the transport closes before completion", () =>
+ Effect.gen(function* () {
+ const test = fixture();
+ const service = yield* test.make;
+ const pending = yield* service
+ .generateThreadTitle(titleInput)
+ .pipe(Effect.flip, Effect.forkChild);
+ yield* Effect.promise(() => test.started);
+ test.closeConnection();
+ expect(yield* Fiber.join(pending)).toMatchObject({ _tag: "TextGenerationError" });
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("releases the host when interrupted after the turn starts", () =>
+ Effect.gen(function* () {
+ const test = fixture();
+ const service = yield* test.make;
+ const fiber = yield* service.generateThreadTitle(titleInput).pipe(Effect.forkChild);
+ yield* Effect.promise(() => test.started);
+ yield* Fiber.interrupt(fiber);
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("denies approval requests and fails the noninteractive request", () =>
+ Effect.gen(function* () {
+ const test = fixture((emit) =>
+ emit("approval/requested", {
+ approvalId: "approval-1",
+ currentRequirementId: "requirement-1",
+ availableChoices: [{ choiceId: "deny-1", decision: "denied" }],
+ }),
+ );
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
+ _tag: "TextGenerationError",
+ });
+ expect(test.host.connection.command).toHaveBeenCalledWith("approval/decide", {
+ sessionId: "session-1",
+ approvalId: "approval-1",
+ requirementId: "requirement-1",
+ choiceId: "deny-1",
+ });
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("cancels user input requests", () =>
+ Effect.gen(function* () {
+ const test = fixture((emit) => emit("userInput/requested", { userInputId: "input-1" }));
+ const service = yield* test.make;
+ expect(yield* service.generateThreadTitle(titleInput).pipe(Effect.flip)).toMatchObject({
+ _tag: "TextGenerationError",
+ });
+ expect(test.host.connection.command).toHaveBeenCalledWith(
+ "userInput/cancel",
+ expect.objectContaining({
+ userInputId: "input-1",
+ }),
+ );
+ expect(test.host.close).toHaveBeenCalledOnce();
+ }),
+ );
+
+ it.effect("generates and sanitizes commit content through Muse", () =>
+ Effect.gen(function* () {
+ const test = fixture(
+ finish('{"subject":"Fix login.","body":" Details ","branch":"fix-login"}'),
+ );
+ const service = yield* test.make;
+ const generated = yield* service.generateCommitMessage({
+ cwd: titleInput.cwd,
+ branch: "main",
+ stagedSummary: "login.ts",
+ stagedPatch: "login change",
+ includeBranch: true,
+ modelSelection,
+ });
+ expect(generated.subject).toBe("Fix login");
+ expect(generated.body).toBe("Details");
+ expect(generated.branch).toContain("fix-login");
+ }),
+ );
+});
diff --git a/packages/provider-muse/src/server/textGeneration.ts b/packages/provider-muse/src/server/textGeneration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a13cf30260c83c5f16e9ff71e9017acc45b8886
--- /dev/null
+++ b/packages/provider-muse/src/server/textGeneration.ts
@@ -0,0 +1,350 @@
+import {
+ DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
+ MUSE_DEFAULT_MODEL,
+ type ModelSelection,
+ type ServerProviderModel,
+ TextGenerationError,
+} from "@t3tools/contracts";
+import type { MuseSettings } from "../settings.ts";
+import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
+import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
+import { extractJsonObject } from "@t3tools/shared/schemaJson";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Option from "effect/Option";
+import * as Schema from "effect/Schema";
+
+import { createMuseSdkHost, createMuseSdkHostEffect, type MuseSdkHost } from "./sdk.ts";
+import { museModelCapabilities, resolveMuseReasoningEffort } from "./modelCatalog.ts";
+import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration";
+import {
+ buildBranchNamePrompt,
+ buildCommitMessagePrompt,
+ buildPrContentPrompt,
+ buildThreadTitlePrompt,
+} from "@t3tools/provider-core/server/textGenerationPrompts";
+import {
+ sanitizeCommitSubject,
+ sanitizePrTitle,
+ sanitizeThreadTitle,
+ toJsonSchemaObject,
+} from "@t3tools/provider-core/server/textGenerationUtils";
+
+const SessionStarted = Schema.Struct({ session: Schema.Struct({ sessionId: Schema.String }) });
+const ItemNotification = Schema.Struct({
+ sessionId: Schema.String,
+ item: Schema.Struct({
+ itemId: Schema.String,
+ kind: Schema.String,
+ revision: Schema.Int,
+ turnId: Schema.optionalKey(Schema.String),
+ text: Schema.optionalKey(Schema.String),
+ retracted: Schema.optionalKey(Schema.Boolean),
+ truncated: Schema.optionalKey(Schema.Boolean),
+ }),
+});
+const TurnCompleted = Schema.Struct({
+ sessionId: Schema.String,
+ turnId: Schema.String,
+ terminal: Schema.String,
+ error: Schema.optionalKey(Schema.Struct({ message: Schema.String })),
+});
+const ApprovalRequested = Schema.Struct({
+ sessionId: Schema.String,
+ approvalId: Schema.String,
+ currentRequirementId: Schema.Unknown,
+ availableChoices: Schema.Array(
+ Schema.Struct({ choiceId: Schema.String, decision: Schema.String }),
+ ),
+});
+const UserInputRequested = Schema.Struct({
+ sessionId: Schema.String,
+ userInputId: Schema.String,
+});
+const decodeSession = Schema.decodeUnknownSync(SessionStarted);
+const decodeItem = Schema.decodeUnknownSync(ItemNotification);
+const decodeCompleted = Schema.decodeUnknownSync(TurnCompleted);
+const decodeApproval = Schema.decodeUnknownSync(ApprovalRequested);
+const decodeUserInput = Schema.decodeUnknownSync(UserInputRequested);
+const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
+const decodeJson = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown));
+
+type Operation = keyof ProviderTextGeneration;
+
+async function generateMuseText(
+ host: MuseSdkHost,
+ workspaceRoot: string,
+ prompt: string,
+ modelId: string | undefined,
+ reasoningEffort: string | undefined,
+ signal: AbortSignal,
+) {
+ let sessionId: string | undefined;
+ const turnId = host.connection.mintCommandId();
+ const messages = new Map();
+ let resolveCompletion: (text: string) => void = () => {};
+ let rejectCompletion: (error: unknown) => void = () => {};
+ const completion = new Promise((resolve, reject) => {
+ resolveCompletion = resolve;
+ rejectCompletion = reject;
+ });
+ // Notifications may settle the turn before the turn/start acknowledgement arrives.
+ void completion.catch(() => {});
+ const rejectInteractive = async (method: string, params: Record) => {
+ if (method === "approval/requested") {
+ const approval = decodeApproval(params);
+ if (approval.sessionId !== sessionId) return;
+ rejectCompletion(new Error("Muse requested an interactive action during text generation."));
+ const deny = approval.availableChoices.find((choice) => choice.decision === "denied");
+ if (deny) {
+ await host.connection.command("approval/decide", {
+ sessionId,
+ approvalId: approval.approvalId,
+ requirementId: approval.currentRequirementId,
+ choiceId: deny.choiceId,
+ });
+ }
+ } else {
+ const request = decodeUserInput(params);
+ if (request.sessionId !== sessionId) return;
+ rejectCompletion(new Error("Muse requested an interactive action during text generation."));
+ await host.connection.command("userInput/cancel", {
+ sessionId,
+ userInputId: request.userInputId,
+ reason: "T3 Code text generation cannot request user input.",
+ });
+ }
+ };
+ host.connection.onNotification((notification) => {
+ try {
+ if (notification.method === "item/completed" || notification.method === "item/updated") {
+ const { item, sessionId: itemSessionId } = decodeItem(notification.params);
+ if (itemSessionId !== sessionId || item.turnId !== turnId || item.kind !== "agentMessage")
+ return;
+ const previous = messages.get(item.itemId);
+ if (!previous || item.revision > previous.revision) messages.set(item.itemId, item);
+ } else if (notification.method === "turn/completed") {
+ const completed = decodeCompleted(notification.params);
+ if (completed.sessionId !== sessionId || completed.turnId !== turnId) return;
+ if (completed.terminal === "completed") {
+ // A turn may include commentary before its final structured answer.
+ const answer = [...messages.values()].findLast((item) => !item.retracted);
+ if (answer?.truncated) {
+ rejectCompletion(new Error("Muse text generation returned a truncated response."));
+ } else {
+ resolveCompletion(answer?.text ?? "");
+ }
+ } else {
+ rejectCompletion(
+ new Error(completed.error?.message ?? `Muse turn ${completed.terminal}.`),
+ );
+ }
+ } else if (
+ notification.method === "approval/requested" ||
+ notification.method === "userInput/requested"
+ ) {
+ void rejectInteractive(notification.method, notification.params ?? {}).catch(
+ rejectCompletion,
+ );
+ } else if (notification.method === "view/gap") {
+ rejectCompletion(new Error("Muse text generation lost part of its response."));
+ }
+ } catch (error) {
+ rejectCompletion(error);
+ }
+ });
+ host.connection.onProtocolError(rejectCompletion);
+ host.connection.onServerRequest(async () => {
+ const error = new Error("T3 Code text generation cannot approve interactive requests.");
+ rejectCompletion(error);
+ throw error;
+ });
+ void host.connection.closed.then(() => {
+ rejectCompletion(new Error("Muse connection closed before text generation completed."));
+ });
+ void host.exited.then(() => {
+ rejectCompletion(new Error("Muse exited before text generation completed."));
+ });
+ const onAbort = () => rejectCompletion(signal.reason);
+ signal.addEventListener("abort", onAbort, { once: true });
+ try {
+ signal.throwIfAborted();
+ const started = decodeSession(
+ await host.connection.command("session/start", {
+ workspaceRoot,
+ providerId: "meta",
+ ...(modelId ? { modelId } : {}),
+ approvalMode: "denyUnmatched",
+ }),
+ );
+ sessionId = started.session.sessionId;
+ const acknowledged = host.connection.command(
+ "turn/start",
+ {
+ sessionId,
+ input: [{ type: "text", text: prompt }],
+ ...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
+ },
+ { commandId: turnId },
+ );
+ return await Promise.race([acknowledged.then(() => completion), completion]);
+ } finally {
+ signal.removeEventListener("abort", onAbort);
+ }
+}
+
+export const makeMuseTextGeneration = Effect.fn("makeMuseTextGeneration")(function* (
+ settings: MuseSettings,
+ options: {
+ readonly environment?: NodeJS.ProcessEnv;
+ readonly modelCatalog?: Effect.Effect>;
+ /** Test hook. */
+ readonly createHost?: typeof createMuseSdkHost;
+ } = {},
+) {
+ const { environment, modelCatalog = Effect.succeed([]), createHost } = options;
+ const fileSystem = yield* FileSystem.FileSystem;
+ const runMuseJson = Effect.fn("runMuseJson")(function* (input: {
+ operation: Operation;
+ prompt: string;
+ outputSchema: S;
+ modelSelection: ModelSelection;
+ }): Effect.fn.Return {
+ const { operation } = input;
+ if (!settings.enabled) {
+ return yield* new TextGenerationError({ operation, detail: "Muse Code is disabled." });
+ }
+ const selectedModel = input.modelSelection.model.trim();
+ const models = yield* modelCatalog;
+ // "default" is the catalog's default model, as in chat. Without one, Muse picks.
+ const catalogModel =
+ selectedModel && selectedModel !== MUSE_DEFAULT_MODEL
+ ? models.find((model) => model.slug === selectedModel)
+ : models.find((model) => model.isDefault && !model.isCustom);
+ const modelId =
+ selectedModel && selectedModel !== MUSE_DEFAULT_MODEL ? selectedModel : catalogModel?.slug;
+ const reasoningEffort = resolveMuseReasoningEffort(
+ catalogModel?.capabilities ?? museModelCapabilities(),
+ getModelSelectionStringOptionValue(input.modelSelection, "reasoningEffort") ??
+ DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
+ );
+ const jsonSchema = yield* encodeJson(toJsonSchemaObject(input.outputSchema)).pipe(
+ Effect.mapError(
+ (cause) => new TextGenerationError({ operation, detail: "Invalid output schema.", cause }),
+ ),
+ );
+ const response = yield* Effect.gen(function* () {
+ // Metadata requests already include their context and do not need checkout configuration.
+ const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-muse-text-" });
+ const host = yield* Effect.acquireRelease(
+ createMuseSdkHostEffect(
+ {
+ binaryPath: settings.binaryPath,
+ cwd,
+ ...(environment ? { environment } : {}),
+ readOnly: true,
+ // Muse derives SDK turn events from its session log, including completion.
+ sessionLogging: true,
+ },
+ createHost,
+ ),
+ (host: MuseSdkHost) => Effect.promise(() => host.close()),
+ { interruptible: true },
+ );
+ return yield* Effect.tryPromise((signal) =>
+ generateMuseText(
+ host,
+ cwd,
+ `${input.prompt}\n\nReturn only a JSON object matching this schema, with no markdown fences. Do not use tools or ask questions.\n${jsonSchema}`,
+ modelId,
+ reasoningEffort,
+ signal,
+ ),
+ );
+ }).pipe(
+ Effect.scoped,
+ Effect.mapError(
+ (cause) =>
+ new TextGenerationError({
+ operation,
+ detail:
+ "Muse Code text generation failed. Check Muse login and availability on this T3 server host.",
+ cause,
+ }),
+ ),
+ Effect.timeoutOption(180_000),
+ Effect.flatMap(
+ Option.match({
+ onNone: () =>
+ Effect.fail(new TextGenerationError({ operation, detail: "Muse request timed out." })),
+ onSome: Effect.succeed,
+ }),
+ ),
+ );
+ const decodeOutput = Schema.decodeEffect(input.outputSchema);
+ return yield* decodeJson(extractJsonObject(response)).pipe(
+ Effect.flatMap(decodeOutput),
+ Effect.mapError(
+ (cause) =>
+ new TextGenerationError({
+ operation,
+ detail: "Muse returned invalid structured output.",
+ cause,
+ }),
+ ),
+ );
+ });
+
+ const generateCommitMessage: ProviderTextGeneration["generateCommitMessage"] = Effect.fn(
+ "MuseTextGeneration.generateCommitMessage",
+ )(function* (input) {
+ const generated = yield* runMuseJson({
+ operation: "generateCommitMessage",
+ ...buildCommitMessagePrompt({ ...input, includeBranch: input.includeBranch === true }),
+ modelSelection: input.modelSelection,
+ });
+ return {
+ subject: sanitizeCommitSubject(generated.subject),
+ body: generated.body.trim(),
+ ...("branch" in generated && typeof generated.branch === "string"
+ ? { branch: sanitizeFeatureBranchName(generated.branch) }
+ : {}),
+ };
+ });
+ const generatePrContent: ProviderTextGeneration["generatePrContent"] = Effect.fn(
+ "MuseTextGeneration.generatePrContent",
+ )(function* (input) {
+ const generated = yield* runMuseJson({
+ operation: "generatePrContent",
+ ...buildPrContentPrompt(input),
+ modelSelection: input.modelSelection,
+ });
+ return { title: sanitizePrTitle(generated.title), body: generated.body.trim() };
+ });
+ const generateBranchName: ProviderTextGeneration["generateBranchName"] = Effect.fn(
+ "MuseTextGeneration.generateBranchName",
+ )(function* (input) {
+ const generated = yield* runMuseJson({
+ operation: "generateBranchName",
+ ...buildBranchNamePrompt(input),
+ modelSelection: input.modelSelection,
+ });
+ return { branch: sanitizeBranchFragment(generated.branch) };
+ });
+ const generateThreadTitle: ProviderTextGeneration["generateThreadTitle"] = Effect.fn(
+ "MuseTextGeneration.generateThreadTitle",
+ )(function* (input) {
+ const generated = yield* runMuseJson({
+ operation: "generateThreadTitle",
+ ...buildThreadTitlePrompt(input),
+ modelSelection: input.modelSelection,
+ });
+ return { title: sanitizeThreadTitle(generated.title) };
+ });
+ return {
+ generateCommitMessage,
+ generatePrContent,
+ generateBranchName,
+ generateThreadTitle,
+ } satisfies ProviderTextGeneration;
+});
diff --git a/packages/provider-muse/src/settings.ts b/packages/provider-muse/src/settings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85a781dc660897ef3a3dab87a94c9fb98fc28847
--- /dev/null
+++ b/packages/provider-muse/src/settings.ts
@@ -0,0 +1,36 @@
+/**
+ * Muse Code instance settings. Shared by the server driver and the client
+ * settings form, so it holds only browser-safe schema code.
+ *
+ * @module provider-muse/settings
+ */
+import {
+ CustomModelSetting,
+ makeBinaryPathSetting,
+ makeProviderSettingsSchema,
+} from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Schema from "effect/Schema";
+
+export const MuseSettings = makeProviderSettingsSchema(
+ {
+ enabled: Schema.Boolean.pipe(
+ Schema.withDecodingDefault(Effect.succeed(false)),
+ Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
+ ),
+ binaryPath: makeBinaryPathSetting("muse").pipe(
+ Schema.annotateKey({
+ title: "Binary path",
+ description:
+ "Path to the Muse Code binary on this environment. Sign in with muse login on that host.",
+ providerSettingsForm: { placeholder: "muse", clearWhenEmpty: "omit" },
+ }),
+ ),
+ customModels: Schema.Array(CustomModelSetting).pipe(
+ Schema.withDecodingDefault(Effect.succeed([])),
+ Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
+ ),
+ },
+ { order: ["binaryPath"] },
+);
+export type MuseSettings = typeof MuseSettings.Type;
diff --git a/packages/provider-muse/src/testing.ts b/packages/provider-muse/src/testing.ts
new file mode 100644
index 0000000000000000000000000000000000000000..810388c5afe984c7d44898409fc9878e998bae7f
--- /dev/null
+++ b/packages/provider-muse/src/testing.ts
@@ -0,0 +1,14 @@
+/**
+ * Muse Code internals for replay tests and the fixture recorder: the MSP
+ * launch arguments, initialize parameters, environment, and version parser.
+ *
+ * @module provider-muse/testing
+ */
+export {
+ makeMuseEnvironment,
+ museInitializeParams,
+ museServeArgs,
+ type MuseSdkHost,
+ type MuseSdkHostOptions,
+} from "./server/sdk.ts";
+export { parseMuseVersion } from "./server/maintenance.ts";
diff --git a/packages/provider-muse/tsconfig.json b/packages/provider-muse/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..374bac55202ddcad695ede1c567ec219a61e9c48
--- /dev/null
+++ b/packages/provider-muse/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["node"]
+ },
+ "include": ["src"]
+}
diff --git a/packages/provider-testing/package.json b/packages/provider-testing/package.json
index 35e3941ba87d70c7a70727a64ae577e958cb92f7..39c6c1f644aa29f192ccdc99016011b5585084d1 100644
--- a/packages/provider-testing/package.json
+++ b/packages/provider-testing/package.json
@@ -18,6 +18,10 @@
"./host": {
"types": "./src/host.ts",
"import": "./src/host.ts"
+ },
+ "./fakeCli": {
+ "types": "./src/fakeCli.ts",
+ "import": "./src/fakeCli.ts"
}
},
"scripts": {
@@ -28,6 +32,7 @@
"@effect/platform-node": "catalog:",
"@t3tools/contracts": "workspace:*",
"@t3tools/provider-core": "workspace:*",
+ "@t3tools/shared": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
diff --git a/packages/provider-testing/src/fakeCli.ts b/packages/provider-testing/src/fakeCli.ts
new file mode 100644
index 0000000000000000000000000000000000000000..68ab157aed7aecb1a0419a896f67ed92cc17db7f
--- /dev/null
+++ b/packages/provider-testing/src/fakeCli.ts
@@ -0,0 +1,106 @@
+// @effect-diagnostics nodeBuiltinImport:off preferSchemaOverJson:off - synchronous fixture writer used from plain and Effect tests alike.
+import * as NodeFS from "node:fs";
+import * as NodePath from "node:path";
+
+import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
+
+export interface FakeCliOptions {
+ /** Directory the launcher and its stub are written into. */
+ readonly directory: string;
+ /** Command name as callers spawn it, e.g. `codex`. */
+ readonly name: string;
+ /** ES module source the launcher runs with `node`. `process.argv.slice(2)` carries the CLI args. */
+ readonly source: string;
+ /** Environment set for the stub before it runs, on top of the inherited environment. */
+ readonly env?: Readonly>;
+ /**
+ * Platform the launcher is shaped for. Effect callers should pass the value
+ * they read from `HostProcessPlatform` so an injected override is honoured;
+ * defaults to the real host for plain tests.
+ */
+ readonly platform?: NodeJS.Platform;
+}
+
+/**
+ * Writes a fake CLI whose behaviour lives in a Node stub. On posix the
+ * launcher is a `#!/bin/sh` script; on Windows it is a `.cmd` shim, since a
+ * shebang file is not executable there and `resolveSpawnCommand` routes
+ * `.cmd` through a shell. Returns the launcher path to hand to the code under
+ * test, which on Windows carries the `.cmd` extension.
+ */
+export function writeFakeCli(options: FakeCliOptions): string {
+ NodeFS.mkdirSync(options.directory, { recursive: true });
+ const stubPath = NodePath.join(options.directory, `${options.name}-stub.mjs`);
+ // The environment rides in a JSON sidecar the stub applies to itself, since
+ // neither sh nor cmd.exe can quote every value (newlines, quotes, `%`) safely.
+ const envPath = NodePath.join(options.directory, `${options.name}-env.json`);
+ NodeFS.writeFileSync(envPath, JSON.stringify(options.env ?? {}), "utf8");
+ NodeFS.writeFileSync(
+ stubPath,
+ [
+ 'import { readFileSync as readFakeCliEnv } from "node:fs";',
+ `Object.assign(process.env, JSON.parse(readFakeCliEnv(${JSON.stringify(envPath)}, "utf8")));`,
+ options.source,
+ ].join("\n"),
+ "utf8",
+ );
+
+ if ((options.platform ?? HostProcessPlatform.defaultValue()) === "win32") {
+ const launcherPath = NodePath.join(options.directory, `${options.name}.cmd`);
+ NodeFS.writeFileSync(
+ launcherPath,
+ ["@echo off", `node "%~dp0${options.name}-stub.mjs" %*`, "exit /b %ERRORLEVEL%", ""].join(
+ "\r\n",
+ ),
+ "utf8",
+ );
+ return launcherPath;
+ }
+
+ const launcherPath = NodePath.join(options.directory, options.name);
+ NodeFS.writeFileSync(
+ launcherPath,
+ ["#!/bin/sh", `exec node "$(dirname "$0")/${options.name}-stub.mjs" "$@"`, ""].join("\n"),
+ "utf8",
+ );
+ NodeFS.chmodSync(launcherPath, 0o755);
+ return launcherPath;
+}
+
+/**
+ * Stub source that becomes `scriptPath`: after optionally requiring a leading
+ * argv prefix, it imports the script into its own process so the stub is the
+ * agent. Nothing sits between the code under test and the mock, so a kill or
+ * a closed stdin reaches it directly and its exit log is faithful on every
+ * host. The script must not read `process.argv`; the mock agents are driven
+ * by environment and stdin.
+ */
+export function execScriptSource(options: {
+ readonly scriptPath: string;
+ readonly expectedArgs?: ReadonlyArray;
+ /** Tab-separated argv appended here per invocation, for launch-flag assertions. */
+ readonly argvLogPath?: string;
+ /** Wait before handing over, for tests that race a slow startup. */
+ readonly delayMs?: number;
+}): string {
+ return [
+ 'import { appendFileSync } from "node:fs";',
+ 'import { pathToFileURL } from "node:url";',
+ "const args = process.argv.slice(2);",
+ ...(options.argvLogPath === undefined
+ ? []
+ : [
+ `appendFileSync(${JSON.stringify(options.argvLogPath)}, args.join(${JSON.stringify("\t")}) + ${JSON.stringify("\n")});`,
+ ]),
+ ...(options.delayMs === undefined
+ ? []
+ : [`Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ${options.delayMs});`]),
+ `const expected = ${JSON.stringify(options.expectedArgs ?? [])};`,
+ "if (expected.some((value, index) => args[index] !== value)) {",
+ ' process.stderr.write(`unexpected args: ${args.join(" ")}\\n`);',
+ " process.exit(11);",
+ "}",
+ `await import(pathToFileURL(${JSON.stringify(options.scriptPath)}).href);`,
+ "",
+ ].join("\n");
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index edc6c3e9a99a562a9dec54c447f3db04179ddf4b..0a4d57f3c47a96e3d76f751df82313856f95f337 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -356,6 +356,9 @@ importers:
'@t3tools/provider-core':
specifier: workspace:*
version: link:../../packages/provider-core
+ '@t3tools/provider-muse':
+ specifier: workspace:*
+ version: link:../../packages/provider-muse
'@t3tools/provider-pi':
specifier: workspace:*
version: link:../../packages/provider-pi
@@ -603,6 +606,9 @@ importers:
'@t3tools/provider-core':
specifier: workspace:*
version: link:../../packages/provider-core
+ '@t3tools/provider-muse':
+ specifier: workspace:*
+ version: link:../../packages/provider-muse
'@t3tools/provider-pi':
specifier: workspace:*
version: link:../../packages/provider-pi
@@ -733,6 +739,9 @@ importers:
'@t3tools/provider-core':
specifier: workspace:*
version: link:../../packages/provider-core
+ '@t3tools/provider-muse':
+ specifier: workspace:*
+ version: link:../../packages/provider-muse
'@t3tools/provider-pi':
specifier: workspace:*
version: link:../../packages/provider-pi
@@ -1099,6 +1108,40 @@ importers:
specifier: 1.0.0
version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ packages/provider-muse:
+ dependencies:
+ '@muse-code/sdk':
+ specifier: 1.4.2
+ version: 1.4.2(@types/node@24.12.4)
+ '@t3tools/contracts':
+ specifier: workspace:*
+ version: link:../contracts
+ '@t3tools/provider-core':
+ specifier: workspace:*
+ version: link:../provider-core
+ '@t3tools/shared':
+ specifier: workspace:*
+ version: link:../shared
+ effect:
+ specifier: 4.0.1
+ version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)
+ devDependencies:
+ '@effect/platform-node':
+ specifier: 4.0.1
+ version: 4.0.1(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(redis@6.2.1)(utf-8-validate@6.0.6)
+ '@effect/vitest':
+ specifier: 4.0.1
+ version: 4.0.1(patch_hash=359f6fb2f7b3ec145bb72208edb9034f02489791aa2491a55cdbd69bd56ee0d2)(@types/node@24.12.4)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f))(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ '@t3tools/provider-testing':
+ specifier: workspace:*
+ version: link:../provider-testing
+ '@types/node':
+ specifier: 24.12.4
+ version: 24.12.4
+ vite-plus:
+ specifier: 1.0.0
+ version: 1.0.0(@types/node@24.12.4)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+
packages/provider-pi:
dependencies:
'@t3tools/contracts':
@@ -1141,6 +1184,9 @@ importers:
'@t3tools/provider-core':
specifier: workspace:*
version: link:../provider-core
+ '@t3tools/shared':
+ specifier: workspace:*
+ version: link:../shared
effect:
specifier: 4.0.1
version: 4.0.1(patch_hash=a33cba07c41f32374c2aaa86ea4a84d3ab9a872a78c94b65e929b19f8361856f)
diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts
index 8fb9ab883995e474cc7f8fe404a2da8114ca8dac..2f39d7a61e93727a4fdde815fc2a0eb7313f6b13 100644
--- a/scripts/release-smoke.ts
+++ b/scripts/release-smoke.ts
@@ -31,6 +31,7 @@ const workspaceFiles = [
"packages/provider-core/package.json",
"packages/provider-testing/package.json",
"packages/provider-pi/package.json",
+ "packages/provider-muse/package.json",
"packages/effect-acp/package.json",
"packages/effect-codex-app-server/package.json",
"scripts/package.json",