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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -3
- package/judge/dist/auth.js +56 -8
- package/judge/dist/mude.js +43 -25
- package/judge/dist/setup.js +47 -29
- package/judge/manifest.json +17 -30
- package/judge/mu/index.js +1024 -549
- package/kyrn/bin/kyrn-doctor +16 -27
- package/kyrn/bin/mu.mjs +94 -112
- package/package.json +2 -2
- package/kyrn/bin/kyrn-judge-local +0 -160
- package/kyrn/local-judge/requirements.lock.txt +0 -26
- package/kyrn/local-judge/server.py +0 -344
package/kyrn/bin/kyrn-doctor
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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" : "
|
|
350
|
+
chrome ? "ok" : "--",
|
|
364
351
|
"browser",
|
|
365
|
-
chrome
|
|
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. "
|
|
26
|
-
// kyrn.json and every KYRN_* variable are still read when the
|
|
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
|
|
104
|
-
"
|
|
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:
|
|
107
|
-
"
|
|
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
|
|
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
|
|
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
|
|
732
|
-
* configuration asks for
|
|
733
|
-
*
|
|
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,
|
|
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
|
|
750
|
-
*
|
|
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
|
|
769
|
-
*
|
|
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(
|
|
772
|
-
|
|
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,
|
|
812
|
-
"
|
|
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
|
-
/**
|
|
851
|
-
export
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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({
|
|
1665
|
-
if (plan.
|
|
1666
|
-
|
|
1667
|
-
|
|
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.
|
|
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": "
|
|
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
|