@xqli02/mneme 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/commands/auto.mjs +115 -32
- package/src/opencode-client.mjs +11 -1
package/package.json
CHANGED
package/src/commands/auto.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* mneme auto — Dual-agent autonomous supervisor loop.
|
|
3
3
|
*
|
|
4
4
|
* Uses two agents in the same opencode session:
|
|
5
|
-
* - Planner (default: gpt-
|
|
5
|
+
* - Planner (default: gpt-4.1): analyzes goal, breaks down tasks, reviews results
|
|
6
6
|
* - Executor (default: claude-opus-4.6): writes code, runs commands, implements changes
|
|
7
7
|
*
|
|
8
8
|
* The planner and executor alternate turns via per-message model switching.
|
|
@@ -35,7 +35,7 @@ import { color, log, run, has } from "../utils.mjs";
|
|
|
35
35
|
|
|
36
36
|
// ── Default models ──────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
const DEFAULT_PLANNER = "github-copilot/gpt-
|
|
38
|
+
const DEFAULT_PLANNER = "github-copilot/gpt-4.1";
|
|
39
39
|
const DEFAULT_EXECUTOR = "github-copilot/claude-opus-4.6";
|
|
40
40
|
|
|
41
41
|
// ── Argument parsing ────────────────────────────────────────────────────────
|
|
@@ -396,6 +396,7 @@ function createEventDisplay(client) {
|
|
|
396
396
|
// Track incremental text and tool display state
|
|
397
397
|
const printedTextLengths = new Map();
|
|
398
398
|
const displayedToolStates = new Map();
|
|
399
|
+
const deltaParts = new Set(); // parts that received delta events
|
|
399
400
|
|
|
400
401
|
async function start() {
|
|
401
402
|
running = true;
|
|
@@ -430,32 +431,61 @@ function createEventDisplay(client) {
|
|
|
430
431
|
const props = event.properties || {};
|
|
431
432
|
|
|
432
433
|
switch (type) {
|
|
434
|
+
// ── Incremental text deltas (streaming) ──
|
|
435
|
+
case "message.part.delta": {
|
|
436
|
+
const partId = props.partID || props.partId;
|
|
437
|
+
if (partId) deltaParts.add(partId);
|
|
438
|
+
if (props.field === "text" && props.delta) {
|
|
439
|
+
process.stdout.write(props.delta);
|
|
440
|
+
lastOutputTime = Date.now();
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Part snapshots (full text at end, tool states) ──
|
|
433
446
|
case "message.part.updated": {
|
|
434
447
|
if (!props.part) break;
|
|
435
448
|
const part = props.part;
|
|
436
449
|
const partId = part.id || `${props.messageID}-${props.index}`;
|
|
437
450
|
|
|
438
|
-
if (
|
|
439
|
-
const prev = printedTextLengths.get(partId) || 0;
|
|
440
|
-
const newText = part.text.slice(prev);
|
|
441
|
-
if (newText) {
|
|
442
|
-
process.stdout.write(newText);
|
|
443
|
-
printedTextLengths.set(partId, part.text.length);
|
|
444
|
-
lastOutputTime = Date.now();
|
|
445
|
-
}
|
|
446
|
-
} else if (
|
|
451
|
+
if (
|
|
447
452
|
part.type === "tool-invocation" ||
|
|
448
453
|
part.type === "tool-result"
|
|
449
454
|
) {
|
|
450
455
|
displayToolPart(part, partId);
|
|
451
456
|
lastOutputTime = Date.now();
|
|
452
457
|
}
|
|
458
|
+
// For text parts: we rely on message.part.delta for streaming,
|
|
459
|
+
// so only use updated as a fallback if we missed the deltas
|
|
460
|
+
if (part.type === "text" && part.text) {
|
|
461
|
+
const prev = printedTextLengths.get(partId) || 0;
|
|
462
|
+
if (prev === 0 && !deltaParts.has(partId)) {
|
|
463
|
+
// We never saw deltas for this part — print the full text
|
|
464
|
+
process.stdout.write(part.text);
|
|
465
|
+
lastOutputTime = Date.now();
|
|
466
|
+
}
|
|
467
|
+
printedTextLengths.set(partId, part.text.length);
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── Session status (busy → idle/completed) ──
|
|
473
|
+
case "session.status": {
|
|
474
|
+
const status = props.status?.type || props.status;
|
|
475
|
+
if (status && status !== "busy" && status !== "pending") {
|
|
476
|
+
if (turnResolve) {
|
|
477
|
+
turnResolve(status);
|
|
478
|
+
turnResolve = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
453
481
|
break;
|
|
454
482
|
}
|
|
455
483
|
|
|
484
|
+
// ── Session updated (metadata; also check for status) ──
|
|
456
485
|
case "session.updated": {
|
|
457
|
-
const
|
|
458
|
-
|
|
486
|
+
const info = props.info || props.session || {};
|
|
487
|
+
const status = info.status?.type || info.status;
|
|
488
|
+
if (status && status !== "busy" && status !== "running" && status !== "pending") {
|
|
459
489
|
if (turnResolve) {
|
|
460
490
|
turnResolve(status);
|
|
461
491
|
turnResolve = null;
|
|
@@ -464,6 +494,17 @@ function createEventDisplay(client) {
|
|
|
464
494
|
break;
|
|
465
495
|
}
|
|
466
496
|
|
|
497
|
+
// ── Message-level finish detection ──
|
|
498
|
+
case "message.updated": {
|
|
499
|
+
const info = props.info || {};
|
|
500
|
+
if (info.finish && info.finish !== "pending") {
|
|
501
|
+
// Model finished generating — mark as turn end candidate
|
|
502
|
+
// (session.status should follow, but use this as backup)
|
|
503
|
+
lastOutputTime = Date.now();
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
|
|
467
508
|
default:
|
|
468
509
|
break;
|
|
469
510
|
}
|
|
@@ -533,6 +574,7 @@ function createEventDisplay(client) {
|
|
|
533
574
|
currentRole = role;
|
|
534
575
|
printedTextLengths.clear();
|
|
535
576
|
displayedToolStates.clear();
|
|
577
|
+
deltaParts.clear();
|
|
536
578
|
}
|
|
537
579
|
|
|
538
580
|
function stop() {
|
|
@@ -584,6 +626,28 @@ async function executeTurn(
|
|
|
584
626
|
// Send async — returns immediately
|
|
585
627
|
await client.session.promptAsync(sessionId, body);
|
|
586
628
|
|
|
629
|
+
// Quick check: if model is invalid, the session may error out almost
|
|
630
|
+
// instantly but promptAsync still returns 204. Poll once after a short
|
|
631
|
+
// delay to catch this before entering the long wait loop.
|
|
632
|
+
await sleep(2000);
|
|
633
|
+
try {
|
|
634
|
+
const sessions = await client.session.list();
|
|
635
|
+
const s = sessions?.find?.((ss) => ss.id === sessionId);
|
|
636
|
+
if (s && s.status && s.status !== "running" && s.status !== "pending") {
|
|
637
|
+
// Session already finished — likely an immediate error
|
|
638
|
+
const msgs = await client.session.messages(sessionId);
|
|
639
|
+
const lastMsg = msgs?.[msgs.length - 1];
|
|
640
|
+
const errInfo = lastMsg?.info?.error;
|
|
641
|
+
if (errInfo) {
|
|
642
|
+
const errMsg = errInfo.data?.message || errInfo.name || "unknown";
|
|
643
|
+
log.fail(`Model error: ${errMsg}`);
|
|
644
|
+
return { status: "error", aborted: true, quit: false };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
// Ignore probe failures — fall through to normal wait
|
|
649
|
+
}
|
|
650
|
+
|
|
587
651
|
const HEARTBEAT_INTERVAL = 15_000; // print elapsed every 15s of silence
|
|
588
652
|
const SILENCE_WARN = 30_000; // warn after 30s of no output
|
|
589
653
|
const SILENCE_ABORT = 120_000; // auto-abort after 120s of no output
|
|
@@ -684,30 +748,49 @@ async function supervisorLoop(client, opts, inputQueue) {
|
|
|
684
748
|
const plannerModel = parseModelSpec(opts.planner);
|
|
685
749
|
const executorModel = parseModelSpec(opts.executor);
|
|
686
750
|
|
|
687
|
-
//
|
|
688
|
-
log.info("Validating models...");
|
|
689
|
-
const modelsOutput = run("opencode models 2>/dev/null") || "";
|
|
690
|
-
const validateModel = (label, spec) => {
|
|
691
|
-
// Look for the model ID in the output; opencode models lists "provider/model"
|
|
692
|
-
if (modelsOutput && !modelsOutput.includes(spec)) {
|
|
693
|
-
log.fail(`${label} model "${spec}" not found in available models.`);
|
|
694
|
-
console.log(color.dim(" Available models:"));
|
|
695
|
-
for (const line of modelsOutput.split("\n").filter(l => l.trim())) {
|
|
696
|
-
console.log(color.dim(` ${line.trim()}`));
|
|
697
|
-
}
|
|
698
|
-
throw new Error(`Invalid ${label.toLowerCase()} model: ${spec}`);
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
validateModel("Planner", opts.planner);
|
|
702
|
-
validateModel("Executor", opts.executor);
|
|
703
|
-
log.ok("Models validated");
|
|
704
|
-
|
|
705
|
-
// Create session
|
|
751
|
+
// Create session first (needed for model probing)
|
|
706
752
|
log.info("Creating session...");
|
|
707
753
|
const session = await client.session.create({ title: "mneme auto" });
|
|
708
754
|
const sessionId = session.id;
|
|
709
755
|
log.ok(`Session: ${sessionId}`);
|
|
710
756
|
|
|
757
|
+
// ── Validate models by sending a real test prompt ──
|
|
758
|
+
// opencode models lists theoretical models, but the provider may reject them
|
|
759
|
+
// at runtime (e.g. Copilot plan doesn't include gpt-5.x). Only a real API
|
|
760
|
+
// call reveals this — sync prompt returns the error, async silently fails.
|
|
761
|
+
log.info("Validating models (API probe)...");
|
|
762
|
+
const probeModels = [
|
|
763
|
+
{ label: "Planner", spec: opts.planner, parsed: plannerModel },
|
|
764
|
+
{ label: "Executor", spec: opts.executor, parsed: executorModel },
|
|
765
|
+
];
|
|
766
|
+
// Deduplicate if both use the same model
|
|
767
|
+
const seen = new Set();
|
|
768
|
+
for (const m of probeModels) {
|
|
769
|
+
if (seen.has(m.spec)) continue;
|
|
770
|
+
seen.add(m.spec);
|
|
771
|
+
try {
|
|
772
|
+
const result = await client.session.prompt(sessionId, {
|
|
773
|
+
parts: [{ type: "text", text: "Say OK" }],
|
|
774
|
+
model: m.parsed,
|
|
775
|
+
});
|
|
776
|
+
// Check if the response contains an error
|
|
777
|
+
const err = result?.info?.error;
|
|
778
|
+
if (err) {
|
|
779
|
+
const msg = err.data?.message || err.name || "unknown error";
|
|
780
|
+
log.fail(`${m.label} model "${m.spec}" rejected by provider: ${msg}`);
|
|
781
|
+
console.log(color.dim(" Tip: run 'opencode models' to see listed models, but not all may be available on your plan."));
|
|
782
|
+
throw new Error(`${m.label} model unavailable: ${msg}`);
|
|
783
|
+
}
|
|
784
|
+
log.ok(`${m.label} model verified: ${m.spec}`);
|
|
785
|
+
} catch (probeErr) {
|
|
786
|
+
if (probeErr.message.includes("unavailable") || probeErr.message.includes("rejected")) {
|
|
787
|
+
throw probeErr; // re-throw our own errors
|
|
788
|
+
}
|
|
789
|
+
// API call itself failed — might be a transient issue
|
|
790
|
+
log.warn(`${m.label} model probe inconclusive: ${probeErr.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
711
794
|
// Start SSE event display
|
|
712
795
|
const eventDisplay = createEventDisplay(client);
|
|
713
796
|
eventDisplay.start().catch(() => {});
|
package/src/opencode-client.mjs
CHANGED
|
@@ -65,7 +65,17 @@ export function createClient(baseUrl) {
|
|
|
65
65
|
} else if (line.startsWith("data:")) {
|
|
66
66
|
const data = line.slice(5).trim();
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
const parsed = JSON.parse(data);
|
|
69
|
+
// opencode sends all fields in data JSON: {type, properties, ...}
|
|
70
|
+
// Merge into currentEvent, but prefer explicit event: line if present
|
|
71
|
+
if (parsed && typeof parsed === "object") {
|
|
72
|
+
if (!currentEvent.type && parsed.type) {
|
|
73
|
+
currentEvent.type = parsed.type;
|
|
74
|
+
}
|
|
75
|
+
currentEvent.properties = parsed.properties || parsed;
|
|
76
|
+
} else {
|
|
77
|
+
currentEvent.properties = parsed;
|
|
78
|
+
}
|
|
69
79
|
} catch {
|
|
70
80
|
currentEvent.properties = data;
|
|
71
81
|
}
|