@melaya/runner 1.0.55 → 1.0.56
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/dist/connection.js +260 -0
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -472,6 +472,266 @@ export async function connect(opts) {
|
|
|
472
472
|
socket.emit("runner:runComplete", { runId: payload.runId, status: "failed" });
|
|
473
473
|
}
|
|
474
474
|
});
|
|
475
|
+
// ── Trading Crew strategy spawn ──────────────────────────────────────
|
|
476
|
+
// Mirrors the runner:run pipeline path but for `strategy_type='agent_crew'`
|
|
477
|
+
// rows whose runtime_mode='local_runner'. The server's
|
|
478
|
+
// StrategyCrewRuntime branches on runtimeMode='local_runner' (decision
|
|
479
|
+
// tree in Obsidian/07 Trading/Trading Strategies/Trading Agents Crew/
|
|
480
|
+
// Local vs Cloud.md), emits `runner:start_crew_strategy` to the user's
|
|
481
|
+
// socket, and this handler boots the same crew worker as the cloud
|
|
482
|
+
// pool would — except spawned as a plain Python subprocess on the
|
|
483
|
+
// user's hardware so lmstudio/ollama personas reach localhost:1234 /
|
|
484
|
+
// localhost:11434 directly. There's no code signature here because the
|
|
485
|
+
// cfg.generated_code originates from the Melaya server (cloud-trusted),
|
|
486
|
+
// not from operator input. Agent message events flow back via the
|
|
487
|
+
// existing runner:event → runnerNamespace.pushEvent path so the FE
|
|
488
|
+
// Studio's RenderRunningCrew bubbles populate in real time.
|
|
489
|
+
socket.on("runner:start_crew_strategy", async (payload) => {
|
|
490
|
+
const runId = payload.run_id;
|
|
491
|
+
const sid = payload.strategy_id;
|
|
492
|
+
const cfg = payload.cfg ?? {};
|
|
493
|
+
console.log(chalk.hex("#10b981")(`\n ▶ Trading crew: ${sid.slice(0, 16)}… (run ${runId.slice(0, 10)}…)`));
|
|
494
|
+
try {
|
|
495
|
+
const mainPyContent = String(cfg.generated_code ?? "").trim();
|
|
496
|
+
if (!mainPyContent) {
|
|
497
|
+
// Server-side fallback should always populate generated_code now
|
|
498
|
+
// (runnerNamespace.startCrewStrategyOnRunner backfills via
|
|
499
|
+
// renderCrewFallbackMainPy). If we still arrive here, the server
|
|
500
|
+
// is older than this runner — surface a clear message instead of
|
|
501
|
+
// a silent no-op.
|
|
502
|
+
console.log(chalk.red(" ✗ Crew cfg missing generated_code — server-side fallback unavailable"));
|
|
503
|
+
socket.emit("runner:event", {
|
|
504
|
+
run_id: runId,
|
|
505
|
+
event_type: "agent_message",
|
|
506
|
+
replyId: `crew-init-${runId}`,
|
|
507
|
+
replyName: "Runner",
|
|
508
|
+
replyRole: "system",
|
|
509
|
+
msg: {
|
|
510
|
+
id: `crew-init-${runId}`,
|
|
511
|
+
name: "Runner",
|
|
512
|
+
role: "system",
|
|
513
|
+
content: [{ type: "text", text: "Crew cfg missing generated_code. Re-save the crew in the Studio." }],
|
|
514
|
+
metadata: {},
|
|
515
|
+
timestamp: new Date().toISOString(),
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
socket.emit("runner:runComplete", { runId, status: "failed" });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Ensure shared modules cached. The crew worker imports from
|
|
522
|
+
// `shared.runtime.trading_crew_personas` (per the codegen
|
|
523
|
+
// template's import_path) which lives in the same shared/ tree
|
|
524
|
+
// the pipeline runner uses.
|
|
525
|
+
await ensureSharedModules(opts.serverUrl, "latest");
|
|
526
|
+
// Dedicated venv with agentscope + anthropic + openai installed.
|
|
527
|
+
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
528
|
+
const envResult = await ensurePythonEnv(opts.pythonPath, "latest", (msg) => {
|
|
529
|
+
console.log(chalk.gray(` [venv] ${msg}`));
|
|
530
|
+
socket.emit("runner:event", {
|
|
531
|
+
run_id: runId,
|
|
532
|
+
event_type: "pipeline_phase",
|
|
533
|
+
project: `crew-${sid.slice(-12)}`,
|
|
534
|
+
step: 1,
|
|
535
|
+
total: 5,
|
|
536
|
+
label: `venv: ${msg}`,
|
|
537
|
+
status: "started",
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
if (!envResult.ok) {
|
|
541
|
+
console.log(chalk.red(` ✗ python venv bootstrap failed: ${envResult.reason}`));
|
|
542
|
+
socket.emit("runner:event", {
|
|
543
|
+
run_id: runId,
|
|
544
|
+
event_type: "agent_message",
|
|
545
|
+
project: `crew-${sid.slice(-12)}`,
|
|
546
|
+
replyId: `venv-${runId}`,
|
|
547
|
+
replyName: "Runner",
|
|
548
|
+
replyRole: "system",
|
|
549
|
+
msg: {
|
|
550
|
+
id: `venv-${runId}`,
|
|
551
|
+
name: "Runner",
|
|
552
|
+
role: "system",
|
|
553
|
+
content: [{ type: "text", text: `Python env bootstrap failed: ${envResult.reason}` }],
|
|
554
|
+
metadata: {},
|
|
555
|
+
timestamp: new Date().toISOString(),
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
socket.emit("runner:runComplete", { runId, status: "failed" });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Register run + set relay project so agent_message → relay →
|
|
562
|
+
// server → FE join-by-runId works the same as for a pipeline.
|
|
563
|
+
const project = `crew-${sid.slice(-12)}`;
|
|
564
|
+
setActiveProject(project);
|
|
565
|
+
socket.emit("runner:registerRun", {
|
|
566
|
+
runId,
|
|
567
|
+
project,
|
|
568
|
+
pipelineName: project,
|
|
569
|
+
});
|
|
570
|
+
// Write main.py + config.json to a per-run scratch dir.
|
|
571
|
+
const runDir = join(tmpdir(), `melaya-crew-${runId}`);
|
|
572
|
+
mkdirSync(runDir, { recursive: true });
|
|
573
|
+
writeFileSync(join(runDir, "main.py"), mainPyContent, "utf-8");
|
|
574
|
+
writeFileSync(join(runDir, "config.json"), JSON.stringify(cfg, null, 2), "utf-8");
|
|
575
|
+
// Preflight the first persona's model so we surface an early
|
|
576
|
+
// failure if (e.g.) LM Studio hasn't loaded the requested
|
|
577
|
+
// qwen3.6-27b. Same flow as pipelines — it just reads the
|
|
578
|
+
// first model entry from config.json.
|
|
579
|
+
const { preflightModel } = await import("./modelLoader.js");
|
|
580
|
+
const preflight = await preflightModel(JSON.stringify(cfg), (msg) => {
|
|
581
|
+
console.log(chalk.gray(` [model] ${msg}`));
|
|
582
|
+
socket.emit("runner:event", {
|
|
583
|
+
run_id: runId,
|
|
584
|
+
event_type: "pipeline_phase",
|
|
585
|
+
project,
|
|
586
|
+
step: 2,
|
|
587
|
+
total: 5,
|
|
588
|
+
label: msg,
|
|
589
|
+
status: "started",
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
if (!preflight.ok) {
|
|
593
|
+
const detail = `${preflight.reason ?? "unknown"}${preflight.hint ? ` — ${preflight.hint}` : ""}`;
|
|
594
|
+
console.log(chalk.red(` ✗ model preflight failed: ${detail}`));
|
|
595
|
+
socket.emit("runner:event", {
|
|
596
|
+
run_id: runId,
|
|
597
|
+
event_type: "agent_message",
|
|
598
|
+
project,
|
|
599
|
+
replyId: `preflight-${runId}`,
|
|
600
|
+
replyName: "Runner",
|
|
601
|
+
replyRole: "system",
|
|
602
|
+
msg: {
|
|
603
|
+
id: `preflight-${runId}`,
|
|
604
|
+
name: "Runner",
|
|
605
|
+
role: "system",
|
|
606
|
+
content: [{ type: "text", text: `Model preflight failed: ${detail}` }],
|
|
607
|
+
metadata: {},
|
|
608
|
+
timestamp: new Date().toISOString(),
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
socket.emit("runner:runComplete", { runId, status: "failed" });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// Build env. Same MEL_MODEL_* scrub + cert bundle wiring as the
|
|
615
|
+
// pipeline path; on top of that, set the MEL_CREW_* the crew loop
|
|
616
|
+
// reads (strategy id, cadence, dry-run mode, owner) plus
|
|
617
|
+
// MEL_OWNER_USER_ID/TIER which the HITL middleware uses for
|
|
618
|
+
// per-tier quota checks.
|
|
619
|
+
const sharedDir = getSharedDir();
|
|
620
|
+
const inherited = { ...process.env };
|
|
621
|
+
for (const k of Object.keys(inherited)) {
|
|
622
|
+
if (k.startsWith("MEL_MODEL_"))
|
|
623
|
+
delete inherited[k];
|
|
624
|
+
}
|
|
625
|
+
const certBundle = (await import("./pythonEnv.js")).getCertBundlePath();
|
|
626
|
+
const sslEnv = certBundle
|
|
627
|
+
? { SSL_CERT_FILE: certBundle, REQUESTS_CA_BUNDLE: certBundle }
|
|
628
|
+
: {};
|
|
629
|
+
const env = {
|
|
630
|
+
...inherited,
|
|
631
|
+
PYTHONPATH: sharedDir,
|
|
632
|
+
PYTHONIOENCODING: "utf-8",
|
|
633
|
+
PYTHONUTF8: "1",
|
|
634
|
+
...sslEnv,
|
|
635
|
+
MEL_RUN_ID: runId,
|
|
636
|
+
MEL_STUDIO_URL: opts.serverUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:"),
|
|
637
|
+
MEL_BUILDER_URL: `http://127.0.0.1:${relay.port}`,
|
|
638
|
+
MEL_RELAY_NONCE: relay.nonce,
|
|
639
|
+
LMSTUDIO_BASE_URL: "http://127.0.0.1:1234",
|
|
640
|
+
OLLAMA_BASE_URL: "http://127.0.0.1:11434",
|
|
641
|
+
MEL_CREW_STRATEGY_ID: sid,
|
|
642
|
+
MEL_CREW_CADENCE_MODE: String(cfg.cadenceMode ?? "hybrid"),
|
|
643
|
+
MEL_CREW_CADENCE_SECONDS: String(cfg.cadenceSeconds ?? 300),
|
|
644
|
+
MEL_CREW_DRY_RUN: String(cfg.dryRunMode ?? "paper"),
|
|
645
|
+
MEL_OWNER_USER_ID: payload.owner_user_id,
|
|
646
|
+
MEL_OWNER_TIER: payload.owner_tier,
|
|
647
|
+
MELAYA_OUTPUT_DIR: (() => {
|
|
648
|
+
const dir = join(homedir(), "Documents", "Melaya-Pipelines", project, "outputs");
|
|
649
|
+
try {
|
|
650
|
+
mkdirSync(dir, { recursive: true });
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
console.log(chalk.yellow(` ! could not create output dir ${dir}: ${e.message}`));
|
|
654
|
+
}
|
|
655
|
+
return dir;
|
|
656
|
+
})(),
|
|
657
|
+
...(preflight.profile
|
|
658
|
+
? {
|
|
659
|
+
MEL_MODEL_TIER: preflight.profile.tier,
|
|
660
|
+
MEL_MODEL_FAMILY: preflight.profile.family ?? "",
|
|
661
|
+
MEL_MODEL_PARAMS_B: String(preflight.profile.paramsBillion ?? ""),
|
|
662
|
+
MEL_MODEL_MAX_TOKENS: String(preflight.profile.recommendedMaxTokens),
|
|
663
|
+
MEL_MODEL_MAX_ITERS: String(preflight.profile.recommendedMaxIters),
|
|
664
|
+
MEL_MODEL_DISABLE_THINKING: preflight.profile.thinkingDefault === "off" ? "1" : "0",
|
|
665
|
+
}
|
|
666
|
+
: {}),
|
|
667
|
+
};
|
|
668
|
+
const proc = spawn(envResult.pythonPath, ["-u", join(runDir, "main.py")], {
|
|
669
|
+
cwd: runDir,
|
|
670
|
+
env,
|
|
671
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
672
|
+
});
|
|
673
|
+
activeProcesses.set(runId, proc);
|
|
674
|
+
const stderrTail = [];
|
|
675
|
+
const STDERR_TAIL_MAX = 60;
|
|
676
|
+
proc.stdout?.on("data", (data) => {
|
|
677
|
+
const line = data.toString().trim();
|
|
678
|
+
if (line && opts.verbose)
|
|
679
|
+
console.log(chalk.gray(` [crew-stdout] ${line}`));
|
|
680
|
+
});
|
|
681
|
+
proc.stderr?.on("data", (data) => {
|
|
682
|
+
const text = data.toString();
|
|
683
|
+
for (const ln of text.split("\n")) {
|
|
684
|
+
const t = ln.trimEnd();
|
|
685
|
+
if (!t)
|
|
686
|
+
continue;
|
|
687
|
+
console.log(chalk.yellow(` [crew-stderr] ${t}`));
|
|
688
|
+
stderrTail.push(t);
|
|
689
|
+
if (stderrTail.length > STDERR_TAIL_MAX)
|
|
690
|
+
stderrTail.shift();
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
proc.on("exit", (code) => {
|
|
694
|
+
activeProcesses.delete(runId);
|
|
695
|
+
const status = code === 0 ? "done" : "failed";
|
|
696
|
+
if (status === "failed" && stderrTail.length > 0) {
|
|
697
|
+
const tail = stderrTail.slice(-15).join("\n");
|
|
698
|
+
socket.emit("runner:event", {
|
|
699
|
+
run_id: runId,
|
|
700
|
+
event_type: "agent_message",
|
|
701
|
+
project,
|
|
702
|
+
replyId: `runner-exit-${runId}`,
|
|
703
|
+
replyName: "Runner",
|
|
704
|
+
replyRole: "system",
|
|
705
|
+
msg: {
|
|
706
|
+
id: `runner-exit-${runId}`,
|
|
707
|
+
name: "Runner",
|
|
708
|
+
role: "system",
|
|
709
|
+
content: [{
|
|
710
|
+
type: "text",
|
|
711
|
+
text: `Crew exited with code ${code}. Last stderr:\n\`\`\`\n${tail}\n\`\`\``,
|
|
712
|
+
}],
|
|
713
|
+
metadata: { exitCode: code },
|
|
714
|
+
timestamp: new Date().toISOString(),
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
socket.emit("runner:event", {
|
|
719
|
+
run_id: runId,
|
|
720
|
+
event_type: "run_status",
|
|
721
|
+
status,
|
|
722
|
+
project,
|
|
723
|
+
});
|
|
724
|
+
setTimeout(() => {
|
|
725
|
+
socket.emit("runner:runComplete", { runId, status });
|
|
726
|
+
}, 200);
|
|
727
|
+
console.log(chalk.gray(` ■ Crew finished (exit ${code})\n`));
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
catch (e) {
|
|
731
|
+
console.log(chalk.red(` ✗ Crew spawn failed: ${e.message}`));
|
|
732
|
+
socket.emit("runner:runComplete", { runId: payload.run_id, status: "failed" });
|
|
733
|
+
}
|
|
734
|
+
});
|
|
475
735
|
// ── RAG Mode B: local-folder ingest (privacy-preserving) ─────────────
|
|
476
736
|
// The user picks "Local folder" in the Agent Builder DocsTab and types
|
|
477
737
|
// a path on their machine (e.g. C:\Users\me\Documents\refs). The server
|