@melaya/runner 1.0.93 → 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:
@@ -205,13 +229,19 @@ def _build_agent():
205
229
  "TikTok). Opening the wrong app is a FAILED task, not a reasonable default. "
206
230
  "After the user authorizes the app, phone_list_apps reflects it immediately — "
207
231
  "re-check and open the REQUESTED app.\n"
208
- "- You are FULLY AUTONOMOUS on the phone: NEVER ask the user a question, "
209
- "never end your turn asking for confirmation or offering a choice make the "
210
- "reasonable decision and DO it (the ONE exception is the EXACT-APP rule above: "
211
- "a missing app is a stop, not a substitution). If a feed is algorithmic, use "
212
- "the app's own Search / Explore to find the right content yourself. Keep going "
213
- "until the task is COMPLETE (e.g. all N comments posted) or you are genuinely "
214
- "blocked; only then report what you did and what (if anything) is blocked.\n"
232
+ "- You are AUTONOMOUS on the phone: for routine choices, make the reasonable "
233
+ "decision and DO it never stall the run by ENDING YOUR TURN to ask in chat. "
234
+ "But when you hit a GENUINE fork you cannot reasonably resolve on your own (which "
235
+ "of several accounts to act as, a truly ambiguous target, a risky irreversible "
236
+ "step), or you want a go/no-go before continuing a long task, call "
237
+ "phone_ask_user(question): it expands the Melaya tile ON THE PHONE into a reply "
238
+ "card and BLOCKS for the user's typed answer WITHOUT ending your turn, so the run "
239
+ "continues seamlessly the moment they reply. Use it SPARINGLY, for real decisions "
240
+ "only — not routine ones. The EXACT-APP rule still holds (a missing app is a stop, "
241
+ "not a substitution). If a feed is algorithmic, use the app's own Search / Explore "
242
+ "to find the right content yourself. Keep going until the task is COMPLETE (e.g. "
243
+ "all N comments posted) or you are genuinely blocked; only then report what you "
244
+ "did and what (if anything) is blocked.\n"
215
245
  if phone_enabled else ""
216
246
  )
217
247
  connector_rule = (
@@ -246,13 +276,16 @@ def _build_agent():
246
276
  model_name=model,
247
277
  provider=provider,
248
278
  api_key="", # model.py resolves the provider's own creds (OAuth off disk / localhost)
249
- # Phone tasks are many-step (each comment screen_tree→tap→type→post), so
250
- # give them room like the device template (40); read-only Q&A needs few.
251
- # Connector WRITE tasks are equally many-step (e.g. "message all my recent
252
- # LinkedIn connections" = 1 list + N sends, each its own reasoning+HITL
253
- # round), so they need the same headroom 8 hit the cap mid-task and
254
- # forced _summarizing() on a half-finished tool batch.
255
- max_iters=40 if (phone_enabled or connector_services) else 8,
279
+ # Phone tasks that act on N items ("like + comment on 10 posts") take
280
+ # ~10-15 tool calls EACH (screen_tree→read→tap→type→post_comment→verify→
281
+ # swipe to the next), so a flat 40 capped a 10-post run at ~3 done and
282
+ # forced a mid-task stop which also ends the turn and tears down the
283
+ # phone working-overlay, snapping the phone back to the Melaya app before
284
+ # the task is finished. Give phone runs real headroom (they are HITL-gated,
285
+ # so each write still needs the user's tap — cost stays bounded). Connector
286
+ # WRITE fan-outs (1 list + N sends, each its own reasoning+HITL round) keep
287
+ # the 40 that fits them; read-only Q&A needs very few.
288
+ max_iters=100 if phone_enabled else (40 if connector_services else 8),
256
289
  reliability=True,
257
290
  bounded_memory=True,
258
291
  )
@@ -441,6 +474,9 @@ def _stdin_reader(q: "queue.Queue[str]") -> None:
441
474
 
442
475
  def main() -> int:
443
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)
444
480
  try:
445
481
  agent = _build_agent()
446
482
  except Exception as exc:
@@ -473,6 +509,13 @@ def main() -> int:
473
509
  continue
474
510
  turn_id = str(req.get("turnId") or "")
475
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"))
476
519
  if not message:
477
520
  _emit(turn_id, "done")
478
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.93",
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,