@melaya/runner 1.0.54 → 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 +280 -0
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -317,6 +317,26 @@ export async function connect(opts) {
|
|
|
317
317
|
MEL_RELAY_NONCE: relay.nonce,
|
|
318
318
|
LMSTUDIO_BASE_URL: "http://127.0.0.1:1234",
|
|
319
319
|
OLLAMA_BASE_URL: "http://127.0.0.1:11434",
|
|
320
|
+
// Tool-output directory convention (2026-06-07). File-producing
|
|
321
|
+
// tools (meme_generate, excel writers, …) read MELAYA_OUTPUT_DIR
|
|
322
|
+
// and default their save_to here. For local-runner pipelines we
|
|
323
|
+
// point it at ~/Documents/Melaya-Pipelines/<pipeline>/outputs/ so
|
|
324
|
+
// the file lands somewhere persistent on the user's box (NOT the
|
|
325
|
+
// ephemeral runDir which the runbook expects to survive across
|
|
326
|
+
// runs). The pipeline_dir copy at <runDir>/outputs/ that codegen
|
|
327
|
+
// would create on cloud-spawn is overridden here because the
|
|
328
|
+
// codegen template uses `os.environ.setdefault(...)` — the
|
|
329
|
+
// runner-set value wins. See AddNewTool.md §2d.
|
|
330
|
+
MELAYA_OUTPUT_DIR: (() => {
|
|
331
|
+
const dir = join(homedir(), "Documents", "Melaya-Pipelines", payload.pipelineName, "outputs");
|
|
332
|
+
try {
|
|
333
|
+
mkdirSync(dir, { recursive: true });
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
console.log(chalk.yellow(` ! could not create output dir ${dir}: ${e.message}`));
|
|
337
|
+
}
|
|
338
|
+
return dir;
|
|
339
|
+
})(),
|
|
320
340
|
// Inject per-pipeline credentials (already scoped by the server)
|
|
321
341
|
...payload.credentials,
|
|
322
342
|
// If a Luma browser bridge is up (i.e. the user has signed in
|
|
@@ -452,6 +472,266 @@ export async function connect(opts) {
|
|
|
452
472
|
socket.emit("runner:runComplete", { runId: payload.runId, status: "failed" });
|
|
453
473
|
}
|
|
454
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
|
+
});
|
|
455
735
|
// ── RAG Mode B: local-folder ingest (privacy-preserving) ─────────────
|
|
456
736
|
// The user picks "Local folder" in the Agent Builder DocsTab and types
|
|
457
737
|
// a path on their machine (e.g. C:\Users\me\Documents\refs). The server
|