@sagentlab/navarch-runtime 0.1.116 → 0.1.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +43 -17
  2. package/bin/muse-mcp-bridge.cjs +3 -0
  3. package/bin/worktree-guard-hook.cjs +74 -2
  4. package/dist/adapters/index.cjs +6 -1
  5. package/dist/adapters/muse.cjs +433 -0
  6. package/dist/agent-auth.cjs +2 -1
  7. package/dist/config.cjs +8 -1
  8. package/dist/conversation/capability.cjs +1 -1
  9. package/dist/exit-conditions.cjs +1 -1
  10. package/dist/muse-mcp-bridge.cjs +167 -0
  11. package/dist/session.cjs +30 -8
  12. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  13. package/node_modules/@muse-code/sdk/README.md +88 -0
  14. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  15. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  16. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  17. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  18. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  19. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  20. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  21. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  22. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  23. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  37. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  38. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  39. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  40. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  41. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  45. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  46. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  47. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  48. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  49. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  50. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  51. package/node_modules/@muse-code/sdk/package.json +47 -0
  52. package/package.json +8 -4
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.bridgeMuseMcp = bridgeMuseMcp;
7
+ exports.main = main;
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_crypto_1 = require("node:crypto");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ function object(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
14
+ }
15
+ /**
16
+ * A tools-only MCP transport bridge. Muse receives only this executable and
17
+ * an expiring task-owned file path. Credentials never enter MSP commands or
18
+ * Muse's durable journal. No new listener, OAuth flow or persistent grant.
19
+ */
20
+ async function bridgeMuseMcp(downstream, upstream) {
21
+ let initializeId;
22
+ let ending = false;
23
+ let failed = false;
24
+ let finish;
25
+ const done = new Promise((resolve) => { finish = resolve; });
26
+ const close = async () => {
27
+ if (ending)
28
+ return;
29
+ ending = true;
30
+ await Promise.allSettled([upstream.close(), downstream.close()]);
31
+ finish();
32
+ };
33
+ const fail = () => {
34
+ if (ending)
35
+ return;
36
+ failed = true;
37
+ // Upstream errors can contain endpoints, headers or provider response
38
+ // bodies. Never forward/log them to Muse or its retained transcript.
39
+ process.stderr.write("Navarch MCP bridge connection failed.\n");
40
+ void close();
41
+ };
42
+ const reject = (message, target) => {
43
+ if ("id" in message)
44
+ void target.send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "This Navarch bridge supports tools only" } }).catch(fail);
45
+ };
46
+ downstream.onmessage = (message) => {
47
+ if (!("method" in message)) {
48
+ reject(message, downstream);
49
+ return;
50
+ }
51
+ if (!["initialize", "notifications/initialized", "notifications/cancelled", "ping", "tools/list", "tools/call"].includes(message.method)) {
52
+ reject(message, downstream);
53
+ return;
54
+ }
55
+ if (message.method === "initialize" && "id" in message) {
56
+ initializeId = message.id;
57
+ // Do not advertise sampling, elicitation, roots, or any other route for
58
+ // an upstream server to ask the unattended model to approve something.
59
+ message = { ...message, params: { ...message.params, capabilities: {} } };
60
+ }
61
+ void upstream.send(message).catch(fail);
62
+ };
63
+ upstream.onmessage = (message) => {
64
+ let outgoing = message;
65
+ if ("method" in message && "id" in message) {
66
+ reject(message, upstream);
67
+ return;
68
+ }
69
+ if ("method" in message && !["notifications/tools/list_changed", "notifications/progress", "notifications/cancelled"].includes(message.method))
70
+ return;
71
+ if ("error" in message) {
72
+ outgoing = { jsonrpc: "2.0", id: message.id, error: { code: message.error.code, message: "Navarch MCP upstream request failed" } };
73
+ }
74
+ if ("result" in message && message.id === initializeId) {
75
+ const result = object(message.result);
76
+ if (!result || typeof result.protocolVersion !== "string") {
77
+ fail();
78
+ return;
79
+ }
80
+ const http = upstream;
81
+ http.setProtocolVersion?.(result.protocolVersion);
82
+ outgoing = { ...message, result: { ...result, capabilities: { ...(object(result.capabilities)?.tools ? { tools: object(result.capabilities).tools } : {}) } } };
83
+ }
84
+ void downstream.send(outgoing).catch(fail);
85
+ };
86
+ upstream.onerror = fail;
87
+ downstream.onerror = fail;
88
+ upstream.onclose = () => { void close(); };
89
+ downstream.onclose = () => { void close(); };
90
+ const onEnd = () => { void close(); };
91
+ process.stdin.once("end", onEnd);
92
+ process.once("SIGTERM", onEnd);
93
+ process.once("SIGINT", onEnd);
94
+ try {
95
+ await upstream.start();
96
+ await downstream.start();
97
+ await done;
98
+ if (failed)
99
+ throw new Error("Navarch MCP bridge connection failed");
100
+ }
101
+ finally {
102
+ await close();
103
+ process.stdin.removeListener("end", onEnd);
104
+ process.removeListener("SIGTERM", onEnd);
105
+ process.removeListener("SIGINT", onEnd);
106
+ }
107
+ }
108
+ async function main(args = process.argv.slice(2)) {
109
+ try {
110
+ const [configPath, name, digest] = args;
111
+ if (!configPath || !node_path_1.default.isAbsolute(configPath) || !name || !digest || !/^[a-f0-9]{64}$/.test(digest) || args.length !== 3)
112
+ throw new Error("Invalid bridge arguments");
113
+ const directory = await (0, promises_1.stat)(node_path_1.default.dirname(configPath));
114
+ if (!directory.isDirectory() || (directory.mode & 0o077) !== 0)
115
+ throw new Error("Non-private bridge directory");
116
+ const file = await (0, promises_1.open)(configPath, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
117
+ let content;
118
+ try {
119
+ const metadata = await file.stat();
120
+ if (!metadata.isFile() || (metadata.mode & 0o077) !== 0 || metadata.size > 1024 * 1024)
121
+ throw new Error("Non-private bridge file");
122
+ content = await file.readFile("utf8");
123
+ }
124
+ finally {
125
+ await file.close();
126
+ }
127
+ // Bind the transport/endpoint/headers to the exact configuration chosen
128
+ // by Navarch. A later file replacement cannot redirect a credential.
129
+ if ((0, node_crypto_1.createHash)("sha256").update(content).digest("hex") !== digest)
130
+ throw new Error("Bridge configuration changed");
131
+ const source = object(JSON.parse(content));
132
+ const server = object(source?.[name]);
133
+ if (!server)
134
+ throw new Error("Missing MCP server");
135
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
136
+ let upstream;
137
+ if (server.transport === "streamableHttp" && typeof server.url === "string") {
138
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
139
+ const endpoint = new URL(server.url);
140
+ if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password)
141
+ throw new Error("Invalid MCP URL");
142
+ upstream = new StreamableHTTPClientTransport(endpoint, {
143
+ requestInit: { headers: server.headers },
144
+ // The SDK's SSE GET path does not inherit requestInit.redirect.
145
+ // Enforce the exact authorized endpoint for every HTTP method.
146
+ fetch: (input, init) => {
147
+ const url = new URL(input);
148
+ if (url.href !== endpoint.href)
149
+ return Promise.reject(new Error("MCP endpoint changed"));
150
+ return fetch(input, { ...init, redirect: "error" });
151
+ },
152
+ });
153
+ }
154
+ else if (server.transport === "stdio" && typeof server.command === "string") {
155
+ const { StdioClientTransport, getDefaultEnvironment } = await import("@modelcontextprotocol/sdk/client/stdio.js");
156
+ upstream = new StdioClientTransport({ command: server.command, args: server.args,
157
+ env: { ...getDefaultEnvironment(), ...server.env }, stderr: "ignore" });
158
+ }
159
+ else
160
+ throw new Error("Unsupported MCP transport");
161
+ await bridgeMuseMcp(new StdioServerTransport(), upstream);
162
+ }
163
+ catch {
164
+ process.stderr.write("Navarch MCP bridge could not start; check its task-owned configuration and installed runtime.\n");
165
+ process.exitCode = 1;
166
+ }
167
+ }
package/dist/session.cjs CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runSession = runSession;
7
7
  exports.toEnvMap = toEnvMap;
8
+ const muse_cjs_1 = require("./adapters/muse.cjs");
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const node_perf_hooks_1 = require("node:perf_hooks");
10
11
  const node_fs_1 = require("node:fs");
@@ -50,7 +51,7 @@ function resolveSessionExecution(config, claimed) {
50
51
  model: runtime === "codex" ? "gpt-6-astra"
51
52
  : runtime === "pi-local" ? "qwen3.8-flash-next-iq2_xs"
52
53
  : runtime === "gemini" ? "auto"
53
- : runtime === "antigravity" || runtime === "opencode" || runtime === "acp" || runtime === "qoder" ? "default"
54
+ : runtime === "antigravity" || runtime === "opencode" || runtime === "acp" || runtime === "qoder" || runtime === "muse" ? "default"
54
55
  : "claude-opus-5-5",
55
56
  reasoning_effort: "medium",
56
57
  };
@@ -447,7 +448,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
447
448
  if (!config.mcpConfigPath && config.sandboxMode === "host"
448
449
  && ["build", "verify", "qa"].includes(task.task_type)
449
450
  && task.required_capabilities?.includes("browser-use")
450
- && ["codex", "claude-code", "antigravity", "opencode", "acp"].includes(runtime)) {
451
+ && ["codex", "claude-code", "antigravity", "opencode", "acp", "muse"].includes(runtime)) {
451
452
  browserEvidence = await (0, browser_evidence_cjs_1.startBrowserEvidence)({
452
453
  apiBase: api.getBaseUrl(), machineToken: api.getToken() ?? "", leaseId,
453
454
  isActive: () => !leaseLost && !sessionClosing && !deps.shutdownSignal?.aborted,
@@ -533,6 +534,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
533
534
  let deliveryCorrectionRetries = 0;
534
535
  let deliveredPullRequest = false;
535
536
  let antigravityResume;
537
+ let museSessionId;
536
538
  while (true) {
537
539
  // Guidance can arrive while the worktree/sandbox is being prepared.
538
540
  // It is already included in deliveredGuidance, so clear the pending
@@ -563,6 +565,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
563
565
  : await (0, adapter_watchdog_cjs_1.runWithActivityWatchdog)(adapter, {
564
566
  prompt: runPrompt,
565
567
  mcpConfigPath,
568
+ mcpMetadataDir: workDir,
566
569
  bin,
567
570
  extraArgs,
568
571
  model: execution.model,
@@ -573,6 +576,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
573
576
  codexGuardArgs,
574
577
  antigravityHostGuard: runtime === "antigravity" && config.sandboxMode === "host" && config.worktreeGuard,
575
578
  antigravityResume,
579
+ museHostGuard: config.sandboxMode === "host" && config.worktreeGuard,
580
+ resumeSessionId: runtime === "muse" ? museSessionId : undefined,
576
581
  opencodeHostGuard: runtime === "opencode" && config.sandboxMode === "host" && config.worktreeGuard,
577
582
  // ACP requires an absolute session cwd. Docker mounts the worktree at
578
583
  // this same absolute path, while the other Docker adapters ignore cwd.
@@ -596,6 +601,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
596
601
  executionReport = { ...executionReport, model: turnResult.model };
597
602
  }
598
603
  antigravityResume = turnResult.antigravityResume;
604
+ if (runtime === "muse")
605
+ museSessionId = turnResult.agentSessionId;
599
606
  attempts.push(turnResult);
600
607
  liveStdout = "";
601
608
  liveTranscript.markDirty();
@@ -626,14 +633,25 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
626
633
  log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
627
634
  return sessionOutcome;
628
635
  }
629
- if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0)
636
+ if (!capacityLimit && !authFailure && !leaseLost && !turnResult.timedOut && pendingGuidance.length > 0) {
637
+ // Muse cancellation kills the whole task-owned container. Guidance
638
+ // still holds the lease, so restart that SAME container and restore
639
+ // its tmpfs environment before the next turn; never launch into a
640
+ // stopped sandbox or substitute an unisolated host process.
641
+ if (runtime === "muse" && sandbox && turnResult.killedByLeaseLoss) {
642
+ const restarted = await sandbox_cjs_1.nodeCommandRunner.run("docker", ["start", sandbox.name], { timeoutMs: 10_000 });
643
+ if (restarted.code !== 0)
644
+ throw new Error("Could not restart the task-owned Muse sandbox after guidance.");
645
+ await sandbox.injectEnv(sessionEnv);
646
+ }
630
647
  continue;
648
+ }
631
649
  const result = {
632
650
  ...turnResult,
633
- tokensIn: sumReportedUsage(attempts, "tokensIn"),
634
- tokensOut: sumReportedUsage(attempts, "tokensOut"),
635
- cacheHitTokensIn: sumReportedUsage(attempts, "cacheHitTokensIn"),
636
- costUsd: sumReportedUsage(attempts, "costUsd"),
651
+ tokensIn: sumReportedUsage(attempts, "tokensIn", runtime === "muse"),
652
+ tokensOut: sumReportedUsage(attempts, "tokensOut", runtime === "muse"),
653
+ cacheHitTokensIn: sumReportedUsage(attempts, "cacheHitTokensIn", runtime === "muse"),
654
+ costUsd: runtime === "muse" ? (0, muse_cjs_1.sumMuseSessionCost)(attempts) : sumReportedUsage(attempts, "costUsd"),
637
655
  };
638
656
  const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({
639
657
  ...result,
@@ -947,6 +965,8 @@ function adapterCommand(config, runtime) {
947
965
  return { bin: config.piBin, extraArgs: config.piExtraArgs };
948
966
  case "qoder":
949
967
  return { bin: config.qoderBin, extraArgs: config.qoderExtraArgs };
968
+ case "muse":
969
+ return { bin: config.museBin, extraArgs: config.museExtraArgs };
950
970
  }
951
971
  }
952
972
  /**
@@ -964,7 +984,9 @@ function cacheSplitFields(tokensIn, cacheHitTokensIn) {
964
984
  const hit = Math.min(cacheHitTokensIn, tokensIn);
965
985
  return { cache_hit_input_tokens: hit, cache_miss_input_tokens: tokensIn - hit };
966
986
  }
967
- function sumReportedUsage(attempts, key) {
987
+ function sumReportedUsage(attempts, key, requireComplete = false) {
988
+ if (requireComplete && attempts.some((attempt) => attempt[key] === undefined))
989
+ return undefined;
968
990
  const reported = attempts.flatMap((attempt) => {
969
991
  const value = attempt[key];
970
992
  return value === undefined ? [] : [value];
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Meta Platforms, Inc. and affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ # `@muse-code/sdk`
2
+
3
+ The TypeScript SDK for Muse Code: a typed Node client for driving a Muse Code
4
+ agent over the Muse Session Protocol (MSP).
5
+
6
+ - Spawn a `muse serve` host, open and resume sessions, and send turns.
7
+ - Stream a turn's items and text deltas as async iterators; await the turn's
8
+ server-authored outcome.
9
+ - Answer tool-approval requests with a handler you register — or register
10
+ none and run under the server's own default-deny.
11
+ - A client-side fold keeps a live, typed view of the session — items, session
12
+ state, pending commands — with gap recovery after dropped deliveries and
13
+ safe same-command retry that can never double-submit.
14
+
15
+ **Zero runtime dependencies**: pure TypeScript over the Node standard
16
+ library. The MSP wire declarations are bundled into the package, so nothing
17
+ else is needed to typecheck against it.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @muse-code/sdk
23
+ npm install -D @types/node # required for TypeScript consumers
24
+ ```
25
+
26
+ Node 20 or newer. `@types/node` is declared as an **optional peer
27
+ dependency**: a JavaScript consumer needs nothing, while a TypeScript
28
+ consumer installs it explicitly — without it the declarations that name
29
+ `ChildProcess`, `Readable` and friends do not resolve.
30
+
31
+ You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
32
+ path passed at spawn time.
33
+
34
+ **This package versions in lockstep with Muse Code**: the SDK version is the
35
+ Muse Code release it ships with — `@muse-code/sdk X.Y.Z` pairs with Muse Code
36
+ `X.Y.Z`. The stable MSP surface follows the product's compatibility posture:
37
+ evolution is additive, and a breaking change follows a documented deprecation
38
+ and changelog path. Surfaces marked experimental (`x-msp-openness`)
39
+ may change or be removed with a changelog entry and no deprecation window. Pin an
40
+ exact version, and read the changelog before moving off it.
41
+
42
+ ## Use
43
+
44
+ ```ts
45
+ import { MuseClient } from "@muse-code/sdk";
46
+
47
+ const client = await MuseClient.spawn({
48
+ museBin: "muse",
49
+ args: ["serve"],
50
+ clientInfo: { name: "my-app", version: "1.0.0" },
51
+ });
52
+
53
+ const session = await client.startSession({ workspaceRoot: process.cwd() });
54
+
55
+ const turn = await session.sendUserTurn({
56
+ input: [{ type: "text", text: "Summarize this repository." }],
57
+ });
58
+ for await (const item of turn.items()) console.log(item);
59
+ await turn.completed;
60
+
61
+ await client.close();
62
+ ```
63
+
64
+ `turn.items()` replays what the session already holds before its live tail;
65
+ `turn.deltas()` is live-only. `turn.completed` settles on every exit, so an
66
+ awaited turn can never hang on a session that ended without an answer.
67
+
68
+ To approve tool use programmatically, register a handler on the session;
69
+ approval choices are server-minted, and a decision the request never offered
70
+ is refused and reported rather than sent:
71
+
72
+ ```ts
73
+ session.onApproval(async (request) => ({
74
+ choiceId: request.availableChoices[0].choiceId,
75
+ }));
76
+ ```
77
+
78
+ Status: developer preview. The facade — spawn, session open/resume, turns,
79
+ approvals, and recovery after dropped deliveries — is complete and covered by
80
+ the conformance suites in the source repository.
81
+
82
+ ## Documentation
83
+
84
+ - Guides, cookbook recipes, and the protocol reference:
85
+ <https://meta-models.github.io/muse-code-sdk/>
86
+ - TypeScript API reference:
87
+ <https://meta-models.github.io/muse-code-sdk/next/generated/sdk/>
88
+ - Source and issues: <https://github.com/meta-models/muse-code-sdk>
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Duplex-agnostic MSP NDJSON connection (spec 14990 FR-012/014/015/016).
3
+ *
4
+ * The transport supplies decoded UTF-8 chunks. Chunks may be empty and may
5
+ * split anywhere — including mid surrogate pair: the byte accounting below
6
+ * reconciles split pairs (FR-012(a)), so transports need not align chunks
7
+ * to character boundaries. The Node child-process binding — which sets
8
+ * stdout's encoding before implementing this interface — is one
9
+ * well-behaved transport, not the general guarantee.
10
+ */
11
+ import type { ErrorKind, ErrorObject, Notification, Request, RequestId } from "../msp.js";
12
+ /**
13
+ * The two-way text stream a `Connection` speaks MSP over.
14
+ *
15
+ * Implement it to run the protocol over any duplex channel: yield decoded
16
+ * UTF-8 chunks on `incoming` and accept outgoing frames through `write`.
17
+ * Chunks may be empty and may split anywhere — even mid surrogate pair — the
18
+ * connection reassembles them. If your transport decodes bytes itself, use a
19
+ * streaming decoder: a multi-byte character split across two byte chunks and
20
+ * decoded separately becomes a replacement character before the connection
21
+ * ever sees it. The SDK's spawned child-process stdio binding sets the stream
22
+ * encoding, so it is safe, and is one ready-made implementation.
23
+ */
24
+ export interface DuplexTransport {
25
+ readonly incoming: AsyncIterable<string>;
26
+ write(chunk: string): Promise<void>;
27
+ /**
28
+ * `flushed` settles once every frame the connection had accepted when
29
+ * `close()` was called has been HANDED to `write()` — submission, not
30
+ * completion. A process-owning transport waits for it inside its own
31
+ * shutdown budget before ending the peer's input, so a frame still queued
32
+ * on a microtask is not dropped; it is never GATED by it, because
33
+ * completion is exactly what a wedged peer withholds. Optional and
34
+ * additive: a transport that ignores it behaves as before.
35
+ */
36
+ close?(flushed?: Promise<void>): void | Promise<void>;
37
+ }
38
+ /**
39
+ * Module-private friend seam (never on the package barrel, like spawn.ts's
40
+ * `ownedTransport`): the submission tail a process-owning transport adopts as
41
+ * its DEFAULT `flushed`, so a close routed around this class — e.g. the
42
+ * public `child.close()` — gets the same step 0 (PR #22819 round 4).
43
+ */
44
+ export declare const submissionTail: unique symbol;
45
+ /**
46
+ * Module-private friend seam, like {@link submissionTail}: a request whose
47
+ * promise also yields the exact response frame it settled on, so a caller
48
+ * that refuses a successful result (the handshake's schema-fingerprint
49
+ * check) can put the whole refused frame on `ProtocolError.line`, the same
50
+ * value every other refusal carries (#50588 review).
51
+ */
52
+ export declare const requestWithFrame: unique symbol;
53
+ /** What {@link requestWithFrame} resolves with: the result and its wire line. */
54
+ export interface FramedResult {
55
+ readonly result: Record<string, unknown>;
56
+ readonly line: string;
57
+ }
58
+ /**
59
+ * Tuning knobs for a `Connection`. Every option is optional: the defaults
60
+ * suit a spawned host, so most applications never pass this at all.
61
+ */
62
+ export interface ConnectionOptions {
63
+ readonly frameLimitBytes?: number;
64
+ readonly mintCommandId?: () => string;
65
+ /** Injectable for deterministic transcript clients; values must be unique in flight. */
66
+ readonly mintRequestId?: () => RequestId;
67
+ }
68
+ /**
69
+ * Per-command overrides for `Connection.command`: pin a command id so a
70
+ * reconnect can replay the same logical command, cap the attempt count, or
71
+ * supply your own retry delay.
72
+ */
73
+ export interface CommandOptions {
74
+ /** Reuse this id for a reconnect/replay of the same logical command. */
75
+ readonly commandId?: string;
76
+ /** Total attempts, including the first. */
77
+ readonly maxAttempts?: number;
78
+ /** Injectable so deterministic tests do not sleep. */
79
+ readonly retryDelay?: (attempt: number, error: MspError) => Promise<void>;
80
+ }
81
+ /** Handles one server-initiated request and resolves with the result to send back. */
82
+ export type ServerRequestHandler = (request: Request) => Promise<Record<string, unknown>>;
83
+ /** Receives every server notification, in arrival order. */
84
+ export type NotificationHandler = (notification: Notification) => void;
85
+ /** Receives each framing or correlation violation the connection detects on its inbound stream. */
86
+ export type ProtocolErrorHandler = (error: ProtocolError) => void;
87
+ /** The one typed error family consumers branch on (INV-012). */
88
+ export declare class MspError extends Error {
89
+ readonly code: number;
90
+ readonly kind: ErrorKind;
91
+ readonly data: Readonly<Record<string, unknown>>;
92
+ readonly retryable: boolean | undefined;
93
+ constructor(error: ErrorObject);
94
+ }
95
+ /** A local framing/correlation violation, never a server-authored MSP error. */
96
+ export declare class ProtocolError extends Error {
97
+ /**
98
+ * The inbound frame (wire line) the violation was detected on, when there
99
+ * is one — the whole line, e.g. the response a pending request refused;
100
+ * an oversized frame is cut to the frame limit. Absent for local state
101
+ * errors (closed connection, EOF) that no frame caused.
102
+ */
103
+ readonly line: string | undefined;
104
+ constructor(message: string, line?: string);
105
+ }
106
+ /** Create the SDK-owned UUIDv7 command-id source for a connection/client. */
107
+ export declare function createUuidV7Mint(): () => string;
108
+ /**
109
+ * One MSP connection over newline-delimited JSON.
110
+ *
111
+ * The connection sends your requests and commands, correlates their
112
+ * responses, enforces the frame-size limit, and hands inbound notifications
113
+ * and server requests to the handlers you register. It is transport-agnostic:
114
+ * pair it with any `DuplexTransport`. Most applications never build one
115
+ * directly — `MuseClient.spawn` or `spawnMspConnection` does it for you;
116
+ * reach for a bare `Connection` when you need the raw notification feed or
117
+ * your own transport.
118
+ */
119
+ export declare class Connection {
120
+ #private;
121
+ constructor(transport: DuplexTransport, options?: ConnectionOptions);
122
+ get closed(): Promise<void>;
123
+ /** See {@link submissionTail}. */
124
+ [submissionTail](): Promise<void>;
125
+ request(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>;
126
+ /** See {@link requestWithFrame}. */
127
+ [requestWithFrame](method: string, params?: Record<string, unknown>): Promise<FramedResult>;
128
+ /**
129
+ * Take one id from THIS connection's single `commandId` minter (INV-013).
130
+ *
131
+ * Exposed for the callers that need the id BEFORE the ack: `sendUserTurn`
132
+ * records its optimistic SS4.13 entry under it, and an entry created only on
133
+ * the ack is invisible for exactly the window it exists to cover. The
134
+ * alternative was a facade-side mint, which is the second minter INV-013
135
+ * forbids — delegating here keeps the single-minter property intact and
136
+ * preserves the injected `mintCommandId` seam for deterministic transcripts.
137
+ *
138
+ * Pair it with `command(..., { commandId })`, which reuses the id instead of
139
+ * minting a second one.
140
+ */
141
+ mintCommandId(): string;
142
+ notify(method: string, params?: Record<string, unknown>): void;
143
+ onServerRequest(handler: ServerRequestHandler): void;
144
+ onNotification(handler: NotificationHandler): void;
145
+ onProtocolError(handler: ProtocolErrorHandler): void;
146
+ flush(): Promise<void>;
147
+ command(method: string, params: Record<string, unknown>, options?: CommandOptions): Promise<Record<string, unknown>>;
148
+ close(): Promise<void>;
149
+ }