mu-agent 0.2.4 → 0.2.6

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.
@@ -14,7 +14,6 @@ import {
14
14
  findOnPath,
15
15
  layoutOf,
16
16
  linkState,
17
- localJudgeSupport,
18
17
  packageEntries,
19
18
  parseEnvFile,
20
19
  platformName,
@@ -143,31 +142,18 @@ if (overrides.length > 0) row("--", "modes", overrides.map(([id, value]) => `${i
143
142
  const ladder = config?.features?.swarm?.models ?? [];
144
143
  row("--", "subagents", ladder.length > 0 ? `model ladder ${ladder.join(" < ")}` : "use the session's model (no ladder configured)");
145
144
 
146
- const judgeSupport = localJudgeSupport({ platform: process.platform, env: process.env, wsl });
147
- if (!asksJudge) {
148
- // Nothing asks the local judge either: whether it is up does not matter.
149
- } else if (tiers.some((tier) => tier.startsWith("laya")) && !judgeSupport.runs && !judgeSupport.url) {
150
- // Not a failure of this installation: the sidecar is Core ML, and there is none for this platform.
151
- row("--", "local judge", `not available on ${platformName({ platform: process.platform, wsl })}: the Laya sidecar needs Core ML (macOS on Apple silicon)`);
152
- row("--", "", "Laya's decision points use the next judge or their fallbacks. Use Jev, an LLM judge or MU_LOCAL_JUDGE_URL=<a server with the same API>");
153
- } else if (tiers.some((tier) => tier.startsWith("laya"))) {
154
- const base = env("LOCAL_JUDGE_URL") ?? `http://127.0.0.1:${env("LOCAL_JUDGE_PORT") ?? 47823}`;
155
- const startedAt = performance.now();
156
- try {
157
- const response = await fetch(`${base}/health`, { signal: AbortSignal.timeout(2000) });
158
- const health = await response.json();
159
- const ms = Math.round(performance.now() - startedAt);
160
- row(health.status === "ok" ? "ok" : "--", "local judge", `${base} · ${health.status} · ${health.model ?? "laya"} · ${ms} ms`);
161
- } catch {
162
- row(
163
- "fix",
164
- "local judge",
165
- judgeSupport.runs
166
- ? `${base} isn't answering: run mu judge start (first time: mu judge setup)`
167
- : `${base} (MU_LOCAL_JUDGE_URL) isn't answering`,
168
- );
169
- }
145
+ // Laya, the local judge mu had until 0.2.4, is no longer built in. The next session takes it out of mu.json (mude 1.0 in
146
+ // its place when it is set up), and skips it in MU_JUDGE (packages/kyrn-judge/src/laya-migration.ts).
147
+ if (tiers.includes("laya") || Object.values(config?.judges ?? {}).some((judge) => judge?.type === "local")) {
148
+ row(
149
+ "--",
150
+ "laya",
151
+ env("JUDGE")
152
+ ? "no longer built into mu: mu skips it in MU_JUDGE. Other decision models: mu judge model add <link>"
153
+ : "no longer built into mu: the next session takes it out of mu.json. Other decision models: mu judge model add <link>",
154
+ );
170
155
  }
156
+
171
157
  // The built-in ways to Jev, as packages/kyrn-judge/src/config.ts has them; mu.json may name others.
172
158
  const JEV_ROUTES = {
173
159
  jev: { type: "jev" },
@@ -359,10 +345,13 @@ const { browserAdvice, findBrowser, thisBrowserHost } = await import(
359
345
  const browserHost = thisBrowserHost();
360
346
  const chrome = findBrowser(browserHost);
361
347
  const [advice, ...moreAdvice] = chrome ? [] : browserAdvice(browserHost).split("\n");
348
+ // Only mu's browser needs one: without it the rest of mu works, so it is not something to fix.
362
349
  row(
363
- chrome ? "ok" : "fix",
350
+ chrome ? "ok" : "--",
364
351
  "browser",
365
- chrome ? `${chrome.executable}${chrome.side === "windows" ? " (on the Windows side, reached over mirrored networking)" : ""}` : advice,
352
+ chrome
353
+ ? `${chrome.executable}${chrome.side === "windows" ? " (on the Windows side, reached over mirrored networking)" : ""}`
354
+ : `optional, for mu's browser. ${advice}`,
366
355
  );
367
356
  for (const line of moreAdvice) row("--", "", line);
368
357
  const canRelate = (tier) =>
package/kyrn/bin/mu.mjs CHANGED
@@ -22,8 +22,9 @@
22
22
  // - mu's name on the agent: PI_PACKAGE_DIR points at ~/.mu/app, a view of packages/coding-agent whose
23
23
  // package.json carries piConfig {name: mu, configDir: .mu}. No upstream file is edited.
24
24
  // - the judgment layer (packages/kyrn-judge), with judges from ~/.mu/agent/mu.json or MU_JUDGE
25
- // (e.g. "laya", "laya,luna", "laya,jev", "off"). When "laya" is among them the sidecar is started (macOS only).
26
- // kyrn.json and every KYRN_* variable are still read when the mu spelling is absent.
25
+ // (e.g. "jev", "mude,jev", "off"). When mude 1.0 or a decision model added from Hugging Face is among them, its
26
+ // server is started in the background (macOS only). kyrn.json and every KYRN_* variable are still read when the
27
+ // mu spelling is absent.
27
28
  // - the repo-root .env (the Jev key), read as data and handed to the agent without ever being printed.
28
29
  // The npm package reads ~/.mu/.env instead: there is no repository around it.
29
30
  import { spawn, spawnSync } from "node:child_process";
@@ -100,13 +101,16 @@ export function usage(platform) {
100
101
  ' mu "prompt" start a session with this prompt',
101
102
  ' mu -p "prompt" run one prompt, print the answer and exit',
102
103
  " mu -c | -r continue the last session | pick a session to resume",
103
- " mu setup connect a model (paste an API key or pick a service) and choose a judge (mu setup --help)",
104
- " mu judge mude install install mude 1.0, mu's own decision model for coding, and make it the judge on this Mac (Apple silicon, 1.8 GB)",
104
+ " mu setup connect a model (paste an API key or pick a service) and choose",
105
+ " a judge (mu setup --help)",
106
+ " mu judge mude install install mude 1.0, mu's own decision model for coding, and make it",
107
+ " the judge on this Mac (Apple silicon, 1.8 GB)",
105
108
  " enable | disable | start | stop | status | uninstall (mu judge mude help)",
106
- " mu judge model <cmd> other decision models from Hugging Face: add <link> | list | use | remove (mu judge model help)",
107
- " mu judge <cmd> manage the local judge, Laya (macOS on Apple silicon): setup | start | stop | status | run",
109
+ " mu judge model <cmd> other decision models from Hugging Face:",
110
+ " add <link> | list | use | remove (mu judge model help)",
108
111
  " mu ledger [n] [--json] print the judge's verdicts from the last n sessions",
109
- " mu import <file>... import Claude Code, Codex and ZCode conversations; --list shows them (mu import --help)",
112
+ " mu import <file>... import Claude Code, Codex and ZCode conversations; --list shows",
113
+ " them (mu import --help)",
110
114
  " mu doctor check the installation, the judges and the connections",
111
115
  " mu auth <cmd> sign-in helper for the desktop app (JSON lines):",
112
116
  " status | login <provider> | logout <provider>",
@@ -422,28 +426,9 @@ export function agentDirFor({ env, muDir, platform }) {
422
426
  );
423
427
  }
424
428
 
425
- /** Whether the configuration asks for the local judge. */
426
- export function wantsLaya({ env, agentDir, platform, readFile }) {
427
- const judges = muEnv("JUDGE", env);
428
- if (judges) return `,${judges},`.includes(",laya");
429
- const path = pathFor(platform);
430
- for (const name of ["mu.json", "kyrn.json"]) {
431
- let text;
432
- try {
433
- text = readFile(path.join(agentDir, name));
434
- } catch {
435
- continue;
436
- }
437
- // Like the file itself: kyrn.json is only looked at when there is no mu.json.
438
- return text.includes('"laya');
439
- }
440
- return false;
441
- }
442
-
443
- /** The Core ML sidecar exists for macOS only. Anything else needs a server of its own behind MU_LOCAL_JUDGE_URL. */
444
429
  /**
445
430
  * Whether the configuration asks for mude (mu judge mude): MU_JUDGE naming it, else `tiers` or a route in mu.json (or
446
- * kyrn.json when there is no mu.json). Its server is then started when a session starts, as Laya's is.
431
+ * kyrn.json when there is no mu.json). Its server is then started when a session starts.
447
432
  */
448
433
  export function wantsMude({ env, agentDir, platform, readFile }) {
449
434
  const judges = muEnv("JUDGE", env);
@@ -487,30 +472,47 @@ export function wantedModel({ env, agentDir, muDir, platform, readFile, exists }
487
472
  return undefined;
488
473
  }
489
474
 
475
+ /**
476
+ * Whether mu.json (or kyrn.json when there is no mu.json) still names Laya, which mu no longer has: `laya` in `tiers`
477
+ * or a route (and no judge of that name of a type mu knows), or a judge of Laya's type, `local`. The judgment layer
478
+ * then puts mude 1.0 in its place when it is set up on this Mac (packages/kyrn-judge/src/laya-migration.ts), so mude's
479
+ * server is started for that session too. MU_JUDGE naming Laya is only skipped.
480
+ */
481
+ export function namesLaya({ env, agentDir, platform, readFile }) {
482
+ if (muEnv("JUDGE", env)) return false;
483
+ const path = pathFor(platform);
484
+ for (const name of ["mu.json", "kyrn.json"]) {
485
+ let config;
486
+ try {
487
+ config = JSON.parse(readFile(path.join(agentDir, name)));
488
+ } catch (error) {
489
+ if (error instanceof SyntaxError) return false;
490
+ continue;
491
+ }
492
+ const judges = config?.judges && typeof config.judges === "object" ? config.judges : {};
493
+ const known = ["jev", "typesafe", "clm", "gateway", "http", "llm", "classifier", "mock"];
494
+ const retired = (tier) =>
495
+ typeof tier === "string" &&
496
+ (judges[tier.trim()]?.type === "local" || (tier.trim() === "laya" && !known.includes(judges.laya?.type)));
497
+ const lists = [config?.tiers, ...Object.values(config?.routes ?? {})];
498
+ return lists.some((tiers) => (Array.isArray(tiers) ? tiers : typeof tiers === "string" ? tiers.split(",") : []).some(retired));
499
+ }
500
+ return false;
501
+ }
502
+
503
+ /** What mude 1.0 needs to answer: its environment, made by `mu judge mude install`, and the key `enable` writes to the .env. */
504
+ export function mudeSetUp({ env, muDir, platform, exists }) {
505
+ return Boolean(env.MU_JUDGE_MUDE_KEY) && exists(pathFor(platform).join(muDir, "local-judge", "mude", "venv", "bin", "python"));
506
+ }
507
+
490
508
  /** What starts the server of the decision model the configuration asks for: `mu judge mude start`, or a model's. */
491
509
  export function localModelStart({ env, agentDir, muDir, platform, readFile, exists }) {
492
510
  if (wantsMude({ env, agentDir, platform, readFile })) return ["start", "--no-wait"];
511
+ if (namesLaya({ env, agentDir, platform, readFile }) && mudeSetUp({ env, muDir, platform, exists })) return ["start", "--no-wait"];
493
512
  const model = wantedModel({ env, agentDir, muDir, platform, readFile, exists });
494
513
  return model ? ["model", "start", model, "--no-wait"] : undefined;
495
514
  }
496
515
 
497
- export function localJudgeSupport({ platform, env, wsl = false }) {
498
- if (platform === "darwin") return { runs: true };
499
- const url = muEnv("LOCAL_JUDGE_URL", env);
500
- const where = platformName({ platform, wsl });
501
- return {
502
- runs: false,
503
- url,
504
- message: [
505
- `The local judge (Laya) is a Core ML sidecar that runs only on macOS on Apple silicon, not on ${where}.`,
506
- "mu works without it: Jev, the default hosted judge, answers instead (free on OpenCode Zen, or with your own key via mu setup); you can also set an LLM judge in mu.json.",
507
- "A local judge for the desktop app is in progress. Without a judge, decision points use their fallbacks.",
508
- "To use a server that implements the same API (GET /health, POST /evaluate), set:",
509
- " MU_LOCAL_JUDGE_URL=http://host:port",
510
- ].join("\n"),
511
- };
512
- }
513
-
514
516
  /** exec replaces this process, as the bash launcher did; spawn is what Windows has. MU_LAUNCH forces either. */
515
517
  export function launchStrategy({ platform, env, canExec }) {
516
518
  const forced = muEnv("LAUNCH", env);
@@ -623,17 +625,6 @@ export function planLaunch({
623
625
  // pi reports a fresh install to pi.dev with its version. mu-agent is not a pi install; asked for, it still can be.
624
626
  if (layout === "package" && childEnv.PI_TELEMETRY === undefined) childEnv.PI_TELEMETRY = "0";
625
627
 
626
- let startJudge = false;
627
- if (wantsLaya({ env: childEnv, agentDir, platform, readFile: fs.readFile })) {
628
- const support = localJudgeSupport({ platform, env: childEnv, wsl });
629
- if (support.runs) startJudge = true;
630
- else if (!support.url) {
631
- notes.push(
632
- `mu: the configuration asks for the local judge (Laya), which runs on macOS only; on ${platformName({ platform, wsl })}, the decision points it would answer use the next judge or their fallbacks (mu judge status)`,
633
- );
634
- }
635
- }
636
-
637
628
  // mude's server, or an added model's, when the configuration asks for it: started without waiting, so a session
638
629
  // never waits for the model to load (what it cannot answer yet goes to the judges behind it). Only a Mac runs it.
639
630
  let startMude;
@@ -652,7 +643,6 @@ export function planLaunch({
652
643
  muDir,
653
644
  appDir,
654
645
  agentDir,
655
- startJudge,
656
646
  startMude,
657
647
  notes,
658
648
  preface: argv[0] === "-h" || argv[0] === "--help" ? `${usage(platform)}\n\npi's help (mu passes all of this through):\n` : undefined,
@@ -688,7 +678,6 @@ export function planHost({ platform, env, argv, root, home, fs, wsl = false, str
688
678
  muDir: launch.muDir,
689
679
  appDir: launch.appDir,
690
680
  agentDir: launch.agentDir,
691
- startJudge: launch.startJudge,
692
681
  startMude,
693
682
  notes: launch.notes,
694
683
  };
@@ -712,10 +701,6 @@ export function planHost({ platform, env, argv, root, home, fs, wsl = false, str
712
701
  };
713
702
  }
714
703
 
715
- /** What mu says when the local judge does not come up. */
716
- const LOCAL_JUDGE_NOT_STARTED =
717
- "mu: the local judge (Laya) didn't start; decision points use the next judge or their fallbacks. Run mu judge status for details.";
718
-
719
704
  /** The part of the preparation that is files: the app view of a checkout, mu's agent folder, the configuration's notes. */
720
705
  function prepareFiles({ plan, platform, root, err }) {
721
706
  const path = pathFor(platform);
@@ -728,26 +713,19 @@ function prepareFiles({ plan, platform, root, err }) {
728
713
 
729
714
  /**
730
715
  * What is done before pi starts, for the command line and the desktop app's runtime host alike: the app view that
731
- * names a checkout mu, mu's agent folder, the configuration's notes (to `err`), and the local judge when the
732
- * configuration asks for it. `bin` is this launcher's folder. The local judge is one shared sidecar: starting it is
733
- * idempotent and never blocks pi.
716
+ * names a checkout mu, mu's agent folder, the configuration's notes (to `err`), and the server of the decision model
717
+ * the configuration asks for (mude 1.0, or one added from Hugging Face), started in the background. Nothing here waits
718
+ * for a judge. `bin` (this launcher's folder) is still taken, for the hosts that pass it.
734
719
  */
735
- export function prepareLaunch({ plan, platform, root, bin, err }) {
720
+ export function prepareLaunch({ plan, platform, root, err }) {
736
721
  prepareFiles({ plan, platform, root, err });
737
722
  startMudeServer(plan);
738
- if (plan.startJudge) {
739
- const started = spawnSync(pathFor(platform).join(bin, "kyrn-judge-local"), ["start"], {
740
- stdio: "ignore",
741
- env: plan.env,
742
- });
743
- if (started.status !== 0) err(LOCAL_JUDGE_NOT_STARTED);
744
- }
745
723
  }
746
724
 
747
725
  /**
748
726
  * mude's server, or an added model's, started in the background (`mu judge mude start --no-wait`): nothing waits for
749
- * it, and it outlives this process, shared by every mu session like Laya. When it is not installed it does nothing (the
750
- * doctor says so).
727
+ * it, and it outlives this process, shared by every mu session. When it is not installed it does nothing (the doctor
728
+ * says so). Until it answers, the judges behind it do: its address refuses the connection at once.
751
729
  */
752
730
  function startMudeServer(plan) {
753
731
  if (!plan.startMude) return;
@@ -765,22 +743,11 @@ function startMudeServer(plan) {
765
743
  }
766
744
 
767
745
  /**
768
- * prepareLaunch for a program that must not stop while the local judge starts, such as the desktop app's main process:
769
- * the same steps and notes, with the local judge started through a child process it waits for without blocking.
746
+ * prepareLaunch for a program that must not stop, such as the desktop app's main process. It is prepareLaunch: it
747
+ * starts nothing it waits for. It stays a promise for the hosts that await it.
770
748
  */
771
- export async function prepareLaunchAsync({ plan, platform, root, bin, err }) {
772
- prepareFiles({ plan, platform, root, err });
773
- startMudeServer(plan);
774
- if (!plan.startJudge) return;
775
- const status = await new Promise((resolve) => {
776
- const child = spawn(pathFor(platform).join(bin, "kyrn-judge-local"), ["start"], {
777
- stdio: "ignore",
778
- env: plan.env,
779
- });
780
- child.once("error", () => resolve(null));
781
- child.once("close", (code) => resolve(code));
782
- });
783
- if (status !== 0) err(LOCAL_JUDGE_NOT_STARTED);
749
+ export async function prepareLaunchAsync(input) {
750
+ prepareLaunch(input);
784
751
  }
785
752
 
786
753
  // ---------------------------------------------------------------------------------------------------------
@@ -808,8 +775,8 @@ export const AUTH_USAGE = [
808
775
  " mu auth status",
809
776
  " mu auth login <provider> [--paste]",
810
777
  " mu auth logout <provider>",
811
- "In a terminal, /login inside mu signs in the same way. With --paste, the sign-in takes the address the browser",
812
- "ends on as pasted instead of listening for it: for when another program (the Codex CLI) holds the callback port.",
778
+ "In a terminal, /login inside mu signs in the same way. With --paste, you paste the address the browser ends on",
779
+ "instead of mu listening for it. Use it when another program, such as the Codex CLI, holds the callback port.",
813
780
  "",
814
781
  "pi's auth commands:",
815
782
  ].join("\n");
@@ -847,13 +814,38 @@ export function planAuth({ platform, env, argv, root, home, execPath, fs, canExe
847
814
  // mu judge
848
815
  // ---------------------------------------------------------------------------------------------------------
849
816
 
850
- /** What `mu judge <cmd>` does here: the macOS script, a look at a server of one's own, or a plain explanation. */
851
- export function planJudge({ platform, env, argv, bin, wsl = false }) {
852
- const args = argv.length > 0 ? argv : ["status"];
853
- const support = localJudgeSupport({ platform, env, wsl });
854
- if (support.runs) return { kind: "script", command: pathFor(platform).join(bin, "kyrn-judge-local"), args };
855
- if (args[0] === "status" && support.url) return { kind: "health", url: `${support.url.replace(/\/+$/, "")}/health` };
856
- return { kind: "unsupported", message: support.message };
817
+ /** `mu judge help`. `mu judge mude …` and `mu judge model …` have their own (packages/kyrn-judge/src/mude). */
818
+ export const JUDGE_USAGE = [
819
+ "mu judge: the decision model that answers mu's judgment calls on this machine.",
820
+ "",
821
+ " mu judge mude <cmd> mude 1.0, mu's own decision model for coding (a Mac with Apple silicon):",
822
+ " install | enable | disable | start | stop | status | uninstall (mu judge mude help)",
823
+ " mu judge model <cmd> other decision models from Hugging Face: add <link> | list | use | remove (mu judge model help)",
824
+ "",
825
+ "Hosted judges (Jev and others) are set with mu setup, or in ~/.mu/agent/mu.json.",
826
+ ].join("\n");
827
+
828
+ /** The commands that ran Laya's sidecar until mu 0.2.4. */
829
+ const LAYA_COMMANDS = ["setup", "start", "stop", "status", "run"];
830
+
831
+ /**
832
+ * What `mu judge <cmd>` does, besides `mude` and `model`: its usage, or a plain explanation. Laya, which `mu judge
833
+ * setup | start | stop | status | run` ran, is no longer built into mu.
834
+ */
835
+ export function planJudge({ argv }) {
836
+ const [command] = argv;
837
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") return { kind: "usage", code: 0, text: JUDGE_USAGE };
838
+ if (LAYA_COMMANDS.includes(command)) {
839
+ return {
840
+ kind: "removed",
841
+ code: 1,
842
+ text: [
843
+ `mu judge ${command} used to manage Laya, which is no longer built into mu.`,
844
+ "For a decision model on this machine, install mude 1.0 (mu judge mude install), or add one from Hugging Face (mu judge model add <link>).",
845
+ ].join("\n"),
846
+ };
847
+ }
848
+ return { kind: "unknown", code: 2, text: `Unknown command: mu judge ${command}\n\n${JUDGE_USAGE}` };
857
849
  }
858
850
 
859
851
  // ---------------------------------------------------------------------------------------------------------
@@ -1661,20 +1653,10 @@ export async function main(argv = process.argv.slice(2)) {
1661
1653
  return handOver({ command: plan.command, args: plan.args, env: plan.env, strategy: "spawn" });
1662
1654
  }
1663
1655
  if (command === "judge") {
1664
- const plan = planJudge({ platform, env, argv: rest, bin, wsl });
1665
- if (plan.kind === "script") return handOver({ command: plan.command, args: plan.args, env, strategy });
1666
- if (plan.kind === "unsupported") {
1667
- err(plan.message);
1668
- return 1;
1669
- }
1670
- try {
1671
- const response = await fetch(plan.url, { signal: AbortSignal.timeout(2000) });
1672
- out(await response.text());
1673
- return 0;
1674
- } catch {
1675
- out(`The local judge server isn't answering at ${plan.url}. Check that it's running.`);
1676
- return 1;
1677
- }
1656
+ const plan = planJudge({ argv: rest });
1657
+ if (plan.code === 0) out(plan.text);
1658
+ else err(plan.text);
1659
+ return plan.code;
1678
1660
  }
1679
1661
  if (command === "ledger") {
1680
1662
  return handOver({ command: process.execPath, args: [path.join(bin, "kyrn-ledger"), ...rest], env, strategy });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "mu: a coding agent that hands its routine decisions to a small, fast judge model, so the main model can keep its attention on the work. Built on pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,6 @@
42
42
  "muBuild": {
43
43
  "pi": "1.3.0",
44
44
  "judge": "0.1.0",
45
- "commit": "86b54d6d8"
45
+ "commit": "bd42ca6ba"
46
46
  }
47
47
  }
@@ -1,160 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Manage the mu local judge sidecar (Laya on Core ML, Apple silicon only).
3
- #
4
- # kyrn/bin/kyrn-judge-local setup create the venv, install pinned deps, download the weights (~680 MB)
5
- # kyrn/bin/kyrn-judge-local start start in the background if it is not already running
6
- # kyrn/bin/kyrn-judge-local stop
7
- # kyrn/bin/kyrn-judge-local status
8
- # kyrn/bin/kyrn-judge-local run run in the foreground
9
- #
10
- # One sidecar serves every mu session on this machine. State lives in ~/.mu/local-judge
11
- # (~/.kyrn/local-judge on a machine whose home has not been moved yet).
12
- set -euo pipefail
13
-
14
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
15
- HOME_DIR="$ROOT/kyrn/local-judge"
16
- MODEL_REPO="aac6fef/laya-multilingual-coreml"
17
- if [[ -d "$HOME/.mu" || ! -d "$HOME/.kyrn" ]]; then MU_DIR="$HOME/.mu"; else MU_DIR="$HOME/.kyrn"; fi
18
- RUN_DIR="${MU_LOCAL_JUDGE_RUN_DIR:-${KYRN_LOCAL_JUDGE_RUN_DIR:-$MU_DIR/local-judge}}"
19
- # A checkout keeps the venv and the weights beside server.py. The npm package (mu-agent) keeps them in mu's
20
- # home instead: npm replaces the whole package folder on every update.
21
- if [[ -d "$ROOT/packages/kyrn-judge" ]]; then STATE_DIR="$HOME_DIR"; else STATE_DIR="$RUN_DIR"; fi
22
- PYTHON="$STATE_DIR/.venv/bin/python"
23
- MODEL_DIR="${MU_LOCAL_JUDGE_MODEL:-${KYRN_LOCAL_JUDGE_MODEL:-$STATE_DIR/models/laya-multilingual-coreml}}"
24
- PORT="${MU_LOCAL_JUDGE_PORT:-${KYRN_LOCAL_JUDGE_PORT:-47823}}"
25
- PID_FILE="$RUN_DIR/judge.pid"
26
- LOG_FILE="$RUN_DIR/judge.log"
27
- URL="http://127.0.0.1:$PORT"
28
-
29
- health() { curl -s --max-time 2 "$URL/health" 2>/dev/null || true; }
30
-
31
- is_ready() { [[ "$(health)" == *'"status": "ok"'* ]]; }
32
-
33
- is_up() { [[ "$(health)" == *'"status"'* ]]; }
34
-
35
- require_setup() {
36
- if [[ ! -x "$PYTHON" || ! -f "$MODEL_DIR/coreml_config.json" ]]; then
37
- echo "The local judge isn't installed yet. Run: mu judge setup" >&2
38
- exit 1
39
- fi
40
- }
41
-
42
- cmd_setup() {
43
- if [[ "$(uname -s)-$(uname -m)" != "Darwin-arm64" ]]; then
44
- echo "The local judge needs a Mac with Apple silicon and macOS 15 or later." >&2
45
- exit 1
46
- fi
47
- command -v uv >/dev/null || { echo "mu judge setup needs uv: https://docs.astral.sh/uv/" >&2; exit 1; }
48
- mkdir -p "$STATE_DIR"
49
- [[ -x "$PYTHON" ]] || uv venv --python 3.12 "$STATE_DIR/.venv"
50
- # The official index on purpose: mirrors lag behind new releases.
51
- uv pip install --python "$PYTHON" --index-url https://pypi.org/simple -r "$HOME_DIR/requirements.lock.txt"
52
- if [[ ! -f "$MODEL_DIR/coreml_config.json" ]]; then
53
- "$PYTHON" - "$MODEL_REPO" "$MODEL_DIR" <<'PY'
54
- import sys
55
- from huggingface_hub import snapshot_download
56
-
57
- snapshot_download(
58
- sys.argv[1],
59
- local_dir=sys.argv[2],
60
- allow_patterns=[
61
- "coreml_config.json", "rl_agent_config.json", "encoder/config.json", "tokenizer/*",
62
- "model.mlpackage/**", "host_weights.safetensors", "README.md", "LICENSE*", "NOTICE*",
63
- ],
64
- )
65
- PY
66
- fi
67
- "$PYTHON" - "$MODEL_DIR" <<'PY'
68
- import json, sys
69
- from pathlib import Path
70
- from laya_coreml.artifacts import verify_files
71
-
72
- directory = Path(sys.argv[1])
73
- manifest = json.loads((directory / "coreml_config.json").read_text())
74
- verify_files(directory, manifest["files"])
75
- print(f"Weights checked against the bundle's manifest ({len(manifest['files'])} files).")
76
- PY
77
- }
78
-
79
- cmd_run() {
80
- require_setup
81
- exec "$PYTHON" "$HOME_DIR/server.py" --model-dir "$MODEL_DIR" --port "$PORT" "$@"
82
- }
83
-
84
- cmd_start() {
85
- require_setup
86
- if is_up; then
87
- echo "The local judge is already running at $URL."
88
- return 0
89
- fi
90
- mkdir -p "$RUN_DIR"
91
- nohup "$PYTHON" "$HOME_DIR/server.py" --model-dir "$MODEL_DIR" --port "$PORT" >>"$LOG_FILE" 2>&1 &
92
- echo $! >"$PID_FILE"
93
- for _ in $(seq 1 120); do
94
- if is_ready; then
95
- echo "The local judge is ready at $URL (pid $(cat "$PID_FILE"), log $LOG_FILE)."
96
- return 0
97
- fi
98
- if ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
99
- echo "The local judge stopped while starting; see $LOG_FILE" >&2
100
- tail -5 "$LOG_FILE" >&2 || true
101
- return 1
102
- fi
103
- sleep 0.5
104
- done
105
- echo "The local judge is still warming up; see $LOG_FILE" >&2
106
- }
107
-
108
- cmd_stop() {
109
- if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
110
- kill "$(cat "$PID_FILE")"
111
- echo "Stopped the local judge (pid $(cat "$PID_FILE"))."
112
- else
113
- echo "The local judge isn't running."
114
- fi
115
- rm -f "$PID_FILE"
116
- }
117
-
118
- cmd_status() {
119
- local body
120
- body="$(health)"
121
- if [[ -z "$body" ]]; then echo "The local judge isn't running at $URL."; return 1; fi
122
- # --json: the server's own answer, for scripts.
123
- if [[ "${1:-}" == "--json" ]]; then echo "$body"; return 0; fi
124
- URL="$URL" node -e '
125
- let text = "";
126
- process.stdin.on("data", (chunk) => (text += chunk)).on("end", () => {
127
- let health;
128
- try { health = JSON.parse(text); } catch { console.log(text.trim()); return; }
129
- const up = Math.round((health.uptimeSeconds ?? 0) / 60);
130
- const uptime = up < 60 ? `${up} min` : `${Math.floor(up / 60)} h ${up % 60} min`;
131
- const state = health.status === "ok" ? "running" : `starting (${health.status})`;
132
- const model = String(health.model ?? "").split("/").pop();
133
- console.log(`The local judge is ${state} at ${process.env.URL}: ${model} on ${health.computeUnits}, pid ${health.pid}, up ${uptime}, ${health.requests} requests.`);
134
- });' <<<"$body"
135
- }
136
-
137
- usage() {
138
- cat <<'USAGE'
139
- mu judge: the local judge, Laya, on Core ML (macOS on Apple silicon).
140
-
141
- mu judge setup install it: a Python environment and the weights (about 680 MB)
142
- mu judge start start it in the background, unless it's already running
143
- mu judge stop stop it
144
- mu judge status [--json] whether it's running, and what it serves
145
- mu judge run run it in the foreground
146
-
147
- One local judge serves every mu session on this machine. To have mu ask it, add "laya" to "tiers" in
148
- mu.json, or start mu with MU_JUDGE=laya.
149
- USAGE
150
- }
151
-
152
- case "${1:-status}" in
153
- setup) cmd_setup ;;
154
- run) shift; cmd_run "$@" ;;
155
- start) cmd_start ;;
156
- stop) cmd_stop ;;
157
- status) shift; cmd_status "$@" ;;
158
- help | -h | --help) usage ;;
159
- *) usage >&2; exit 2 ;;
160
- esac
@@ -1,26 +0,0 @@
1
- anyio==4.15.1
2
- attrs==26.1.0
3
- cattrs==26.2.0
4
- certifi==2026.7.22
5
- click==8.5.0
6
- coremltools==9.0
7
- filelock==4.0.1
8
- fsspec==2026.9.0
9
- h11==0.16.0
10
- hf-xet==1.6.0
11
- httpcore==1.0.9
12
- httpx==0.28.1
13
- huggingface-hub==1.32.0
14
- idna==3.20
15
- laya-coreml==0.1.0
16
- mpmath==1.3.0
17
- numpy==2.1.3
18
- packaging==26.3
19
- protobuf==7.36.2
20
- pyaml==26.7.0
21
- pyyaml==6.0.3
22
- safetensors==0.8.0
23
- sympy==1.14.0
24
- tokenizers==0.23.2
25
- tqdm==4.70.1
26
- typing-extensions==4.16.0