@melaya/runner 1.0.25 → 1.0.27
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 +54 -2
- package/dist/pythonEnv.d.ts +17 -0
- package/dist/pythonEnv.js +112 -0
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -104,6 +104,58 @@ export async function connect(opts) {
|
|
|
104
104
|
}
|
|
105
105
|
// Ensure shared modules are cached
|
|
106
106
|
await ensureSharedModules(opts.serverUrl, payload.sharedVersion);
|
|
107
|
+
// Ensure the dedicated venv at ~/.melaya-runner/venv/ has
|
|
108
|
+
// agentscope + its transitive deps installed. Without this, the
|
|
109
|
+
// operator's system python3 typically doesn't have shortuuid /
|
|
110
|
+
// anthropic / openai / opentelemetry / etc. pre-installed and
|
|
111
|
+
// every pipeline run crashes during `import agentscope` with a
|
|
112
|
+
// ModuleNotFoundError. The venv is created lazily on first run
|
|
113
|
+
// and reused until sharedVersion changes.
|
|
114
|
+
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
115
|
+
const envResult = await ensurePythonEnv(opts.pythonPath, payload.sharedVersion, (msg) => {
|
|
116
|
+
console.log(chalk.gray(` [venv] ${msg}`));
|
|
117
|
+
// Surface bootstrap progress to the FE so the launching
|
|
118
|
+
// animation reflects the real state instead of staying on a
|
|
119
|
+
// generic spinner during the 1-2 min first-time install.
|
|
120
|
+
socket.emit("runner:event", {
|
|
121
|
+
runId: payload.runId,
|
|
122
|
+
payload: {
|
|
123
|
+
event_type: "pipeline_phase",
|
|
124
|
+
run_id: payload.runId,
|
|
125
|
+
project: payload.project,
|
|
126
|
+
step: 1,
|
|
127
|
+
total: 5,
|
|
128
|
+
label: `venv: ${msg}`,
|
|
129
|
+
status: "started",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
if (!envResult.ok) {
|
|
134
|
+
console.log(chalk.red(` ✗ python venv bootstrap failed: ${envResult.reason}`));
|
|
135
|
+
socket.emit("runner:event", {
|
|
136
|
+
runId: payload.runId,
|
|
137
|
+
payload: {
|
|
138
|
+
event_type: "agent_message",
|
|
139
|
+
run_id: payload.runId,
|
|
140
|
+
project: payload.project,
|
|
141
|
+
replyId: `venv-${payload.runId}`,
|
|
142
|
+
replyName: "Runner",
|
|
143
|
+
replyRole: "system",
|
|
144
|
+
msg: {
|
|
145
|
+
id: `venv-${payload.runId}`,
|
|
146
|
+
name: "Runner",
|
|
147
|
+
role: "system",
|
|
148
|
+
content: [{ type: "text", text: `Python env bootstrap failed: ${envResult.reason}` }],
|
|
149
|
+
metadata: {},
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
socket.emit("runner:runComplete", { runId: payload.runId, status: "failed" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Replace the system python with the venv python for THIS spawn.
|
|
158
|
+
const pipelinePython = envResult.pythonPath;
|
|
107
159
|
// Register run in DB + set active project for event relay
|
|
108
160
|
setActiveProject(payload.project);
|
|
109
161
|
socket.emit("runner:registerRun", {
|
|
@@ -199,8 +251,8 @@ export async function connect(opts) {
|
|
|
199
251
|
}
|
|
200
252
|
: {}),
|
|
201
253
|
};
|
|
202
|
-
// Spawn Python subprocess
|
|
203
|
-
const proc = spawn(
|
|
254
|
+
// Spawn Python subprocess (uses the bootstrapped venv python).
|
|
255
|
+
const proc = spawn(pipelinePython, ["-u", join(runDir, "main.py")], {
|
|
204
256
|
cwd: runDir,
|
|
205
257
|
env,
|
|
206
258
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage a dedicated Python venv at ~/.melaya-runner/venv/ so pipelines
|
|
3
|
+
* don't depend on whatever's globally installed in the operator's
|
|
4
|
+
* `python3`. Without this, every fresh runner install hits import errors
|
|
5
|
+
* (`ModuleNotFoundError: No module named 'shortuuid'`, etc.) on the
|
|
6
|
+
* first pipeline run because agentscope and its 23 transitive deps are
|
|
7
|
+
* not present in the operator's system Python.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-uses the venv if a marker file matches the current
|
|
10
|
+
* shared-bundle version. Re-bootstraps when the bundle version changes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function venvPython(): string;
|
|
13
|
+
export declare function ensurePythonEnv(systemPython: string, expectedVersion: string, onProgress?: (msg: string) => void): Promise<{
|
|
14
|
+
ok: boolean;
|
|
15
|
+
pythonPath: string;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage a dedicated Python venv at ~/.melaya-runner/venv/ so pipelines
|
|
3
|
+
* don't depend on whatever's globally installed in the operator's
|
|
4
|
+
* `python3`. Without this, every fresh runner install hits import errors
|
|
5
|
+
* (`ModuleNotFoundError: No module named 'shortuuid'`, etc.) on the
|
|
6
|
+
* first pipeline run because agentscope and its 23 transitive deps are
|
|
7
|
+
* not present in the operator's system Python.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-uses the venv if a marker file matches the current
|
|
10
|
+
* shared-bundle version. Re-bootstraps when the bundle version changes.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir, platform } from "os";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
const CACHE_DIR = join(homedir(), ".melaya-runner");
|
|
18
|
+
const VENV_DIR = join(CACHE_DIR, "venv");
|
|
19
|
+
const VENV_MARK = join(CACHE_DIR, "venv-version.txt");
|
|
20
|
+
const AGENTSCOPE = join(CACHE_DIR, "agentscope");
|
|
21
|
+
// Extra deps the cached `shared/tools/*.py` import that aren't in
|
|
22
|
+
// agentscope's pyproject. Keep this list narrow — full tool-specific
|
|
23
|
+
// SDKs (linkedin_api, twilio, stripe, etc.) install lazily on first
|
|
24
|
+
// use via tool-specific scripts. The minimum to GET A PIPELINE RUNNING
|
|
25
|
+
// is agentscope + aiohttp + requests + redis (used by some tools).
|
|
26
|
+
const EXTRA_DEPS = ["aiohttp", "requests", "python-dotenv"];
|
|
27
|
+
export function venvPython() {
|
|
28
|
+
return platform() === "win32"
|
|
29
|
+
? join(VENV_DIR, "Scripts", "python.exe")
|
|
30
|
+
: join(VENV_DIR, "bin", "python");
|
|
31
|
+
}
|
|
32
|
+
function venvIsValid(expectedVersion) {
|
|
33
|
+
if (!existsSync(venvPython()))
|
|
34
|
+
return false;
|
|
35
|
+
if (!existsSync(VENV_MARK))
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const cached = readFileSync(VENV_MARK, "utf-8").trim();
|
|
39
|
+
return cached === expectedVersion;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function runProc(cmd, args, onLine) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
const chew = (b) => {
|
|
49
|
+
for (const ln of b.toString().split("\n")) {
|
|
50
|
+
const t = ln.trimEnd();
|
|
51
|
+
if (t)
|
|
52
|
+
onLine(t);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
child.stdout?.on("data", chew);
|
|
56
|
+
child.stderr?.on("data", chew);
|
|
57
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
58
|
+
child.on("error", () => resolve(1));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function ensurePythonEnv(systemPython, expectedVersion, onProgress = (m) => console.log(chalk.gray(` [venv] ${m}`))) {
|
|
62
|
+
if (venvIsValid(expectedVersion)) {
|
|
63
|
+
return { ok: true, pythonPath: venvPython() };
|
|
64
|
+
}
|
|
65
|
+
if (!existsSync(AGENTSCOPE)) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
pythonPath: systemPython,
|
|
69
|
+
reason: `agentscope cache missing at ${AGENTSCOPE} — restart the runner so the shared bundle downloads first.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
73
|
+
// Step 1: create the venv if missing.
|
|
74
|
+
if (!existsSync(venvPython())) {
|
|
75
|
+
onProgress(`creating venv at ${VENV_DIR} (one-time, ~5s)`);
|
|
76
|
+
const code = await runProc(systemPython, ["-m", "venv", VENV_DIR], onProgress);
|
|
77
|
+
if (code !== 0 || !existsSync(venvPython())) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
pythonPath: systemPython,
|
|
81
|
+
reason: `python -m venv failed (exit ${code}). Confirm the venv module is available: ${systemPython} -m venv --help`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Step 2: upgrade pip + wheel so wheel-based deps install cleanly.
|
|
86
|
+
onProgress("upgrading pip / wheel inside the venv");
|
|
87
|
+
await runProc(venvPython(), ["-m", "pip", "install", "--quiet", "--upgrade", "pip", "wheel"], onProgress);
|
|
88
|
+
// Step 3: install agentscope from the local cache (its pyproject pulls
|
|
89
|
+
// anthropic, openai, shortuuid, opentelemetry-*, etc.).
|
|
90
|
+
onProgress("installing agentscope + 23 transitive deps (anthropic, openai, opentelemetry, …) — first run can take 1-2 min");
|
|
91
|
+
const acode = await runProc(venvPython(), ["-m", "pip", "install", "--quiet", "-e", AGENTSCOPE, ...EXTRA_DEPS], onProgress);
|
|
92
|
+
if (acode !== 0) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
pythonPath: systemPython,
|
|
96
|
+
reason: `pip install agentscope failed (exit ${acode}). Check the lines above for the actual pip error.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
writeFileSync(VENV_MARK, expectedVersion, "utf-8");
|
|
100
|
+
// sanity: confirm shortuuid resolves now (it's the historic canary
|
|
101
|
+
// import that fails on a stale env).
|
|
102
|
+
const probe = await runProc(venvPython(), ["-c", "import shortuuid, agentscope, anthropic, openai"], onProgress);
|
|
103
|
+
if (probe !== 0) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
pythonPath: systemPython,
|
|
107
|
+
reason: "venv created but agentscope import probe failed. Delete ~/.melaya-runner/venv and restart the runner.",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
onProgress(`✓ venv ready (python=${venvPython()})`);
|
|
111
|
+
return { ok: true, pythonPath: venvPython() };
|
|
112
|
+
}
|