@melaya/runner 1.0.94 → 1.0.96
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/assistantHost.py +38 -1
- package/dist/connection.js +53 -3
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -95,6 +95,30 @@ if not isinstance(sys.stdout, _RedactingStdout):
|
|
|
95
95
|
sys.stdout = _RedactingStdout(sys.stdout)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
_HITL_MODES = ("safe", "autonomous", "payments_only")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _apply_hitl_mode(mode: str | None) -> None:
|
|
102
|
+
"""Set the assistant autonomy mode env AND mirror it into MEL_HITL_MODE.
|
|
103
|
+
|
|
104
|
+
The runner spawns this host with MEL_ASSISTANT_HITL_MODE (the assistant's
|
|
105
|
+
autonomy mode). The connector-write gate (lazy_registry._maybe_gate_write)
|
|
106
|
+
reads MEL_ASSISTANT_HITL_MODE, but phone.py._cmd — running INSIDE this same
|
|
107
|
+
host process — stamps the device command body from MEL_HITL_MODE. So we
|
|
108
|
+
MIRROR the assistant mode into MEL_HITL_MODE here, giving the phone tools
|
|
109
|
+
the operator's choice too. Called once at boot and again per turn (the TS
|
|
110
|
+
side passes an updated `hitl_mode` on each turn payload; absent ⇒ keep the
|
|
111
|
+
current/spawn value). Unknown / empty ⇒ "safe" (fail-safe)."""
|
|
112
|
+
if mode is None:
|
|
113
|
+
# Boot-time mirror: honour whatever the runner exported at spawn.
|
|
114
|
+
mode = os.environ.get("MEL_ASSISTANT_HITL_MODE", "safe")
|
|
115
|
+
normalized = (str(mode or "safe")).strip().lower()
|
|
116
|
+
if normalized not in _HITL_MODES:
|
|
117
|
+
normalized = "safe"
|
|
118
|
+
os.environ["MEL_ASSISTANT_HITL_MODE"] = normalized
|
|
119
|
+
os.environ["MEL_HITL_MODE"] = normalized
|
|
120
|
+
|
|
121
|
+
|
|
98
122
|
def _emit(turn_id: str, kind: str, **fields) -> None:
|
|
99
123
|
"""Write one structured event line to stdout (flushed) for the runner to relay."""
|
|
100
124
|
try:
|
|
@@ -213,7 +237,10 @@ def _build_agent():
|
|
|
213
237
|
"phone_ask_user(question): it expands the Melaya tile ON THE PHONE into a reply "
|
|
214
238
|
"card and BLOCKS for the user's typed answer WITHOUT ending your turn, so the run "
|
|
215
239
|
"continues seamlessly the moment they reply. Use it SPARINGLY, for real decisions "
|
|
216
|
-
"only — not routine ones.
|
|
240
|
+
"only — not routine ones. Do NOT call phone_ask_user to cope with repeated tool "
|
|
241
|
+
"ERRORS or because you feel stuck/struggling — that is a failure to REPORT, not a "
|
|
242
|
+
"decision for the user; stop and report what failed instead. Only ask for a genuine "
|
|
243
|
+
"who/which fork. The EXACT-APP rule still holds (a missing app is a stop, "
|
|
217
244
|
"not a substitution). If a feed is algorithmic, use the app's own Search / Explore "
|
|
218
245
|
"to find the right content yourself. Keep going until the task is COMPLETE (e.g. "
|
|
219
246
|
"all N comments posted) or you are genuinely blocked; only then report what you "
|
|
@@ -450,6 +477,9 @@ def _stdin_reader(q: "queue.Queue[str]") -> None:
|
|
|
450
477
|
|
|
451
478
|
def main() -> int:
|
|
452
479
|
_log(f"booting (provider={os.environ.get('MEL_ASSISTANT_PROVIDER')})")
|
|
480
|
+
# Mirror the spawn-time assistant autonomy mode into MEL_HITL_MODE so the
|
|
481
|
+
# in-process phone tools (phone.py._cmd) see it from turn one.
|
|
482
|
+
_apply_hitl_mode(None)
|
|
453
483
|
try:
|
|
454
484
|
agent = _build_agent()
|
|
455
485
|
except Exception as exc:
|
|
@@ -482,6 +512,13 @@ def main() -> int:
|
|
|
482
512
|
continue
|
|
483
513
|
turn_id = str(req.get("turnId") or "")
|
|
484
514
|
message = str(req.get("message") or "")
|
|
515
|
+
# Per-turn autonomy mode: the TS side carries an updated `hitl_mode`
|
|
516
|
+
# on runner:assistant_turn so a mid-session flip takes effect next
|
|
517
|
+
# turn. Absent ⇒ keep the current (spawn / previous-turn) mode. The
|
|
518
|
+
# connector gate + phone tools read the env at call-time, so setting
|
|
519
|
+
# it here (before _run_turn) is enough — no agent rebuild needed.
|
|
520
|
+
if "hitl_mode" in req:
|
|
521
|
+
_apply_hitl_mode(req.get("hitl_mode"))
|
|
485
522
|
if not message:
|
|
486
523
|
_emit(turn_id, "done")
|
|
487
524
|
continue
|
package/dist/connection.js
CHANGED
|
@@ -77,11 +77,42 @@ export async function connect(opts) {
|
|
|
77
77
|
const localVersion = getLocalSharedVersion();
|
|
78
78
|
if (!localVersion)
|
|
79
79
|
return; // nothing cached yet — let assistant_start do it
|
|
80
|
-
const { ensurePythonEnv } = await import("./pythonEnv.js");
|
|
81
|
-
await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
|
|
80
|
+
const { ensurePythonEnv, getCertBundlePath } = await import("./pythonEnv.js");
|
|
81
|
+
const envResult = await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
|
|
82
82
|
console.log(chalk.gray(` [assistant prewarm] ${m}`)); });
|
|
83
83
|
if (opts.verbose)
|
|
84
84
|
console.log(chalk.gray(" [assistant prewarm] ready"));
|
|
85
|
+
// ALSO pre-import the heavy Python stack so the first real run/chat
|
|
86
|
+
// doesn't pay the ~6s cold `import shared.runtime.registry`. Spawn it
|
|
87
|
+
// detached + fire-and-forget with the SAME PYTHONPATH/env a real
|
|
88
|
+
// pipeline spawn uses (see the runner:run handler below) so `shared.*`
|
|
89
|
+
// resolves; this heats the OS file cache + bytecode. Never blocks
|
|
90
|
+
// connect, never throws — swallow everything. Only when a shared
|
|
91
|
+
// bundle exists locally (mirror the ensurePythonEnv guard above).
|
|
92
|
+
if (envResult.ok) {
|
|
93
|
+
try {
|
|
94
|
+
const sharedDir = getSharedDir();
|
|
95
|
+
if (existsSync(sharedDir)) {
|
|
96
|
+
const certBundle = getCertBundlePath();
|
|
97
|
+
const warmEnv = {
|
|
98
|
+
...process.env,
|
|
99
|
+
PYTHONPATH: sharedDir,
|
|
100
|
+
PYTHONIOENCODING: "utf-8",
|
|
101
|
+
PYTHONUTF8: "1",
|
|
102
|
+
...(certBundle ? { SSL_CERT_FILE: certBundle, REQUESTS_CA_BUNDLE: certBundle } : {}),
|
|
103
|
+
};
|
|
104
|
+
const warmChild = spawn(envResult.pythonPath, ["-c", "import shared.runtime.registry"], { env: warmEnv, stdio: "ignore" });
|
|
105
|
+
warmChild.on("error", () => { });
|
|
106
|
+
warmChild.on("exit", (code) => {
|
|
107
|
+
if (code === 0 && opts.verbose) {
|
|
108
|
+
console.log(chalk.gray(" [assistant prewarm] import-warmed"));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
warmChild.unref();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { /* fire-and-forget — never throw */ }
|
|
115
|
+
}
|
|
85
116
|
}
|
|
86
117
|
catch (e) {
|
|
87
118
|
if (opts.verbose)
|
|
@@ -907,6 +938,21 @@ export async function connect(opts) {
|
|
|
907
938
|
MEL_ASSISTANT_CONNECTORS: Array.isArray(payload.connectors) ? payload.connectors.join(",") : "",
|
|
908
939
|
// Gate write connector-tools behind the in-chat approval card (fail-safe).
|
|
909
940
|
MEL_ASSISTANT_CONNECTOR_HITL: Array.isArray(payload.connectors) && payload.connectors.length ? "1" : "",
|
|
941
|
+
// HITL autonomy mode ("safe" | "autonomous" | "payments_only"). The host
|
|
942
|
+
// reads this at start; each turn's frame can flip it (see assistant_turn).
|
|
943
|
+
// Fail-safe: unknown ⇒ "safe".
|
|
944
|
+
MEL_ASSISTANT_HITL_MODE: (payload.hitlMode === "autonomous" || payload.hitlMode === "payments_only") ? payload.hitlMode : "safe",
|
|
945
|
+
// Run context for phone-driving assistant turns. The assistant host has
|
|
946
|
+
// no pipeline MEL_RUN_ID, so without this phone.py._cmd omits `run_id`,
|
|
947
|
+
// the server's arm block (phone.ts, `if (runId)`) never fires, and the
|
|
948
|
+
// device never receives `hitlMode` — Autonomous still shows publish
|
|
949
|
+
// cards on the runner-assistant surface. Use the SAME stable
|
|
950
|
+
// `assistant:{userId}` runId the cloud assistant path uses (sid is
|
|
951
|
+
// `userId:conversationId`) so the arm / kill / active-run plumbing stays
|
|
952
|
+
// consistent across surfaces. assistantHost mirrors MEL_ASSISTANT_HITL_MODE
|
|
953
|
+
// → MEL_HITL_MODE so phone.py stamps the mode on the command body.
|
|
954
|
+
MEL_RUN_ID: `assistant:${sid.split(":")[0]}`,
|
|
955
|
+
MEL_PIPELINE_NAME: "Melaya · Assistant",
|
|
910
956
|
// Luma browser bridge (same injection as pipeline runs, line ~431): lets
|
|
911
957
|
// shared/tools/luma.py route /event/register through Playwright and bypass
|
|
912
958
|
// Cloudflare's write rule. Reads keep using aiohttp. Empty when no signed-in
|
|
@@ -970,8 +1016,12 @@ export async function connect(opts) {
|
|
|
970
1016
|
return;
|
|
971
1017
|
}
|
|
972
1018
|
s.lastActivity = Date.now();
|
|
1019
|
+
// Per-turn hitl_mode: the host applies it to os.environ["MEL_ASSISTANT_HITL_MODE"]
|
|
1020
|
+
// (mirrored to MEL_HITL_MODE) BEFORE running the turn, so a mid-session flip
|
|
1021
|
+
// takes effect on the very next message. Fail-safe: unknown ⇒ "safe".
|
|
1022
|
+
const hitlMode = (payload.hitlMode === "autonomous" || payload.hitlMode === "payments_only") ? payload.hitlMode : "safe";
|
|
973
1023
|
try {
|
|
974
|
-
s.proc.stdin?.write(JSON.stringify({ turnId: payload.turnId, message: payload.message }) + "\n");
|
|
1024
|
+
s.proc.stdin?.write(JSON.stringify({ turnId: payload.turnId, message: payload.message, hitl_mode: hitlMode }) + "\n");
|
|
975
1025
|
}
|
|
976
1026
|
catch (e) {
|
|
977
1027
|
socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: `turn write failed: ${e?.message || e}` });
|