@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.
- package/dist/assistantHost.py +57 -14
- package/dist/connection.js +20 -1
- 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:
|
|
@@ -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
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
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
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
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
|
package/dist/connection.js
CHANGED
|
@@ -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}` });
|