@melaya/runner 1.0.94 → 1.0.95

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.
@@ -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:
@@ -450,6 +474,9 @@ def _stdin_reader(q: "queue.Queue[str]") -> None:
450
474
 
451
475
  def main() -> int:
452
476
  _log(f"booting (provider={os.environ.get('MEL_ASSISTANT_PROVIDER')})")
477
+ # Mirror the spawn-time assistant autonomy mode into MEL_HITL_MODE so the
478
+ # in-process phone tools (phone.py._cmd) see it from turn one.
479
+ _apply_hitl_mode(None)
453
480
  try:
454
481
  agent = _build_agent()
455
482
  except Exception as exc:
@@ -482,6 +509,13 @@ def main() -> int:
482
509
  continue
483
510
  turn_id = str(req.get("turnId") or "")
484
511
  message = str(req.get("message") or "")
512
+ # Per-turn autonomy mode: the TS side carries an updated `hitl_mode`
513
+ # on runner:assistant_turn so a mid-session flip takes effect next
514
+ # turn. Absent ⇒ keep the current (spawn / previous-turn) mode. The
515
+ # connector gate + phone tools read the env at call-time, so setting
516
+ # it here (before _run_turn) is enough — no agent rebuild needed.
517
+ if "hitl_mode" in req:
518
+ _apply_hitl_mode(req.get("hitl_mode"))
485
519
  if not message:
486
520
  _emit(turn_id, "done")
487
521
  continue
@@ -907,6 +907,21 @@ export async function connect(opts) {
907
907
  MEL_ASSISTANT_CONNECTORS: Array.isArray(payload.connectors) ? payload.connectors.join(",") : "",
908
908
  // Gate write connector-tools behind the in-chat approval card (fail-safe).
909
909
  MEL_ASSISTANT_CONNECTOR_HITL: Array.isArray(payload.connectors) && payload.connectors.length ? "1" : "",
910
+ // HITL autonomy mode ("safe" | "autonomous" | "payments_only"). The host
911
+ // reads this at start; each turn's frame can flip it (see assistant_turn).
912
+ // Fail-safe: unknown ⇒ "safe".
913
+ MEL_ASSISTANT_HITL_MODE: (payload.hitlMode === "autonomous" || payload.hitlMode === "payments_only") ? payload.hitlMode : "safe",
914
+ // Run context for phone-driving assistant turns. The assistant host has
915
+ // no pipeline MEL_RUN_ID, so without this phone.py._cmd omits `run_id`,
916
+ // the server's arm block (phone.ts, `if (runId)`) never fires, and the
917
+ // device never receives `hitlMode` — Autonomous still shows publish
918
+ // cards on the runner-assistant surface. Use the SAME stable
919
+ // `assistant:{userId}` runId the cloud assistant path uses (sid is
920
+ // `userId:conversationId`) so the arm / kill / active-run plumbing stays
921
+ // consistent across surfaces. assistantHost mirrors MEL_ASSISTANT_HITL_MODE
922
+ // → MEL_HITL_MODE so phone.py stamps the mode on the command body.
923
+ MEL_RUN_ID: `assistant:${sid.split(":")[0]}`,
924
+ MEL_PIPELINE_NAME: "Melaya · Assistant",
910
925
  // Luma browser bridge (same injection as pipeline runs, line ~431): lets
911
926
  // shared/tools/luma.py route /event/register through Playwright and bypass
912
927
  // Cloudflare's write rule. Reads keep using aiohttp. Empty when no signed-in
@@ -970,8 +985,12 @@ export async function connect(opts) {
970
985
  return;
971
986
  }
972
987
  s.lastActivity = Date.now();
988
+ // Per-turn hitl_mode: the host applies it to os.environ["MEL_ASSISTANT_HITL_MODE"]
989
+ // (mirrored to MEL_HITL_MODE) BEFORE running the turn, so a mid-session flip
990
+ // takes effect on the very next message. Fail-safe: unknown ⇒ "safe".
991
+ const hitlMode = (payload.hitlMode === "autonomous" || payload.hitlMode === "payments_only") ? payload.hitlMode : "safe";
973
992
  try {
974
- s.proc.stdin?.write(JSON.stringify({ turnId: payload.turnId, message: payload.message }) + "\n");
993
+ s.proc.stdin?.write(JSON.stringify({ turnId: payload.turnId, message: payload.message, hitl_mode: hitlMode }) + "\n");
975
994
  }
976
995
  catch (e) {
977
996
  socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: `turn write failed: ${e?.message || e}` });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.94",
3
+ "version": "1.0.95",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,