@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.
Files changed (2) hide show
  1. package/dist/connection.js +260 -0
  2. package/package.json +1 -1
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,