@melaya/runner 1.1.78 → 1.1.80
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 +100 -4
- package/dist/pythonEnv.js +19 -3
- package/dist/sharedVendor.d.ts +2 -0
- package/dist/sharedVendor.js +61 -7
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -229,7 +229,42 @@ def _render_summary_text(summary) -> str:
|
|
|
229
229
|
# STOP flag: set by the stdin-reader thread, polled by the running turn.
|
|
230
230
|
_stream = {"turnId": "", "lens": {}, "cancel": False, "usedBrowser": False,
|
|
231
231
|
"deltaBuf": "", "deltaMid": "", "deltaAt": 0.0,
|
|
232
|
-
"thinkLens": {}, "toolIds": set()
|
|
232
|
+
"thinkLens": {}, "toolIds": set(),
|
|
233
|
+
# Progress watchdog (see _progress_watchdog): what the turn is doing
|
|
234
|
+
# right now and when the client last heard from it.
|
|
235
|
+
"activity": "", "lastEmitAt": 0.0, "turnStartAt": 0.0}
|
|
236
|
+
|
|
237
|
+
# One event line at a time: the progress watchdog writes from its own thread.
|
|
238
|
+
_EMIT_LOCK = threading.Lock()
|
|
239
|
+
|
|
240
|
+
# A turn that is silent this long gets a visible "Still working" status line
|
|
241
|
+
# (2026-11-05, conv 33d90517: a Luma → Google Sheet turn went quiet for 5+
|
|
242
|
+
# minutes and the user pressed STOP four times, never seeing what it was doing).
|
|
243
|
+
_PROGRESS_SILENCE_S = 20.0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _fmt_elapsed(seconds: float) -> str:
|
|
247
|
+
s = int(max(0, seconds))
|
|
248
|
+
return f"{s // 60}m {s % 60:02d}s" if s >= 60 else f"{s}s"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _progress_watchdog(turn_id: str, stop: "threading.Event") -> None:
|
|
252
|
+
"""While a turn runs, say what it is doing whenever it has been silent for
|
|
253
|
+
_PROGRESS_SILENCE_S: one `think` line in the chat and one line in the runner
|
|
254
|
+
log. Never raises, never touches the turn itself."""
|
|
255
|
+
while not stop.wait(5.0):
|
|
256
|
+
try:
|
|
257
|
+
if _stream.get("turnId") != turn_id:
|
|
258
|
+
return
|
|
259
|
+
now = time.monotonic()
|
|
260
|
+
if now - float(_stream.get("lastEmitAt") or now) < _PROGRESS_SILENCE_S:
|
|
261
|
+
continue
|
|
262
|
+
activity = _stream.get("activity") or "waiting for the model"
|
|
263
|
+
line = f"Still working ({_fmt_elapsed(now - float(_stream.get('turnStartAt') or now))}): {activity}"
|
|
264
|
+
_log(f"progress turn={turn_id[:8]} {line}")
|
|
265
|
+
_emit(turn_id, "think", content=f"\n{line}\n")
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
233
268
|
|
|
234
269
|
# Coalesce per-token pre_print fragments before emitting a `delta`. agentscope
|
|
235
270
|
# hands us one cumulative snapshot per model chunk; a streaming Anthropic/OpenAI
|
|
@@ -689,8 +724,12 @@ def _emit(turn_id: str, kind: str, **fields) -> None:
|
|
|
689
724
|
"""Write one structured event line to stdout (flushed) for the runner to relay."""
|
|
690
725
|
try:
|
|
691
726
|
payload = {"turnId": turn_id, "kind": kind, **fields}
|
|
692
|
-
|
|
693
|
-
|
|
727
|
+
line = _EVENT_PREFIX + json.dumps(payload, ensure_ascii=False, default=str) + "\n"
|
|
728
|
+
with _EMIT_LOCK:
|
|
729
|
+
sys.stdout.write(line)
|
|
730
|
+
sys.stdout.flush()
|
|
731
|
+
if turn_id:
|
|
732
|
+
_stream["lastEmitAt"] = time.monotonic()
|
|
694
733
|
except Exception:
|
|
695
734
|
# Never let event emission crash the turn loop.
|
|
696
735
|
pass
|
|
@@ -761,6 +800,24 @@ def _emit_turn_files(turn_id: str) -> int:
|
|
|
761
800
|
return n
|
|
762
801
|
|
|
763
802
|
|
|
803
|
+
def _last_tool_input_size(content) -> "tuple[str, int]":
|
|
804
|
+
"""(name, characters of its arguments so far) of the last tool call in a
|
|
805
|
+
streamed message, so a long silent argument write can be reported."""
|
|
806
|
+
blocks = content if isinstance(content, (list, tuple)) else [content]
|
|
807
|
+
for b in reversed(list(blocks)):
|
|
808
|
+
btype = b.get("type") if isinstance(b, dict) else getattr(b, "type", None)
|
|
809
|
+
if btype != "tool_use":
|
|
810
|
+
continue
|
|
811
|
+
name = b.get("name") if isinstance(b, dict) else getattr(b, "name", "")
|
|
812
|
+
args = b.get("input") if isinstance(b, dict) else getattr(b, "input", None)
|
|
813
|
+
try:
|
|
814
|
+
size = len(args) if isinstance(args, str) else len(json.dumps(args or {}, default=str))
|
|
815
|
+
except Exception:
|
|
816
|
+
size = 0
|
|
817
|
+
return str(name or ""), size
|
|
818
|
+
return "", 0
|
|
819
|
+
|
|
820
|
+
|
|
764
821
|
def _split_stream_blocks(content):
|
|
765
822
|
"""Split a streamed message's content into (answer_text, thinking_text,
|
|
766
823
|
[(tool_id, tool_name)]).
|
|
@@ -1430,6 +1487,16 @@ def _register_stream_hooks(agent) -> None:
|
|
|
1430
1487
|
return
|
|
1431
1488
|
mid = str(getattr(msg, "id", "") or id(msg))
|
|
1432
1489
|
text, thinking, tool_uses = _split_stream_blocks(getattr(msg, "content", msg))
|
|
1490
|
+
# What the progress watchdog reports if the turn goes quiet.
|
|
1491
|
+
if tool_uses:
|
|
1492
|
+
_tname_now, _tchars = _last_tool_input_size(getattr(msg, "content", msg))
|
|
1493
|
+
if _tname_now:
|
|
1494
|
+
_stream["activity"] = (f"writing the {_tname_now} call ({_tchars // 1024} KB so far)"
|
|
1495
|
+
if _tchars >= 1024 else f"writing the {_tname_now} call")
|
|
1496
|
+
elif text:
|
|
1497
|
+
_stream["activity"] = "writing the answer"
|
|
1498
|
+
elif thinking:
|
|
1499
|
+
_stream["activity"] = "thinking"
|
|
1433
1500
|
# Reasoning (Codex reasoning summaries, Claude thinking) streams to the
|
|
1434
1501
|
# thinking strip as `think`, never into the answer bubble.
|
|
1435
1502
|
if thinking:
|
|
@@ -1531,8 +1598,29 @@ def _register_stream_hooks(agent) -> None:
|
|
|
1531
1598
|
except Exception:
|
|
1532
1599
|
return False
|
|
1533
1600
|
|
|
1601
|
+
def _pre_acting_hook(_self, kw, *_rest) -> None:
|
|
1602
|
+
# Progress only: which tool is running now (no event of its own).
|
|
1603
|
+
try:
|
|
1604
|
+
if getattr(_self, "name", "") != "Assistant" or not _stream.get("turnId") or not isinstance(kw, dict):
|
|
1605
|
+
return
|
|
1606
|
+
tc = kw.get("tool_call") or {}
|
|
1607
|
+
name = tc.get("name", "") if isinstance(tc, dict) else getattr(tc, "name", "")
|
|
1608
|
+
if name:
|
|
1609
|
+
_stream["activity"] = f"running {name}"
|
|
1610
|
+
except Exception:
|
|
1611
|
+
pass
|
|
1612
|
+
|
|
1613
|
+
def _progress_after_tool(_self, kw, *_rest) -> None:
|
|
1614
|
+
try:
|
|
1615
|
+
if getattr(_self, "name", "") == "Assistant" and _stream.get("turnId"):
|
|
1616
|
+
_stream["activity"] = "waiting for the model"
|
|
1617
|
+
except Exception:
|
|
1618
|
+
pass
|
|
1619
|
+
|
|
1534
1620
|
ok_a = _reg("pre_print", "mel_assistant_delta", _pre_print_hook)
|
|
1535
1621
|
ok_b = _reg("post_acting", "mel_assistant_tool", _post_acting_hook)
|
|
1622
|
+
_reg("pre_acting", "mel_assistant_progress", _pre_acting_hook)
|
|
1623
|
+
_reg("post_acting", "mel_assistant_progress", _progress_after_tool)
|
|
1536
1624
|
_HOOKS_REGISTERED = ok_a or ok_b
|
|
1537
1625
|
_log(f"stream hooks registered (delta={ok_a} tool={ok_b})")
|
|
1538
1626
|
|
|
@@ -1575,6 +1663,11 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
|
|
|
1575
1663
|
_stream["deltaMid"] = ""
|
|
1576
1664
|
_stream["cancel"] = False # fresh turn — clear any stale STOP
|
|
1577
1665
|
_stream["usedBrowser"] = False
|
|
1666
|
+
_stream["activity"] = "waiting for the model"
|
|
1667
|
+
_stream["turnStartAt"] = _stream["lastEmitAt"] = time.monotonic()
|
|
1668
|
+
_progress_stop = threading.Event()
|
|
1669
|
+
threading.Thread(target=_progress_watchdog, args=(turn_id, _progress_stop),
|
|
1670
|
+
name="mel-progress", daemon=True).start()
|
|
1578
1671
|
|
|
1579
1672
|
# Reset the per-turn sub-agent delegation budget so the breadth cap is per
|
|
1580
1673
|
# turn, not per session. Best-effort: never let a stale/absent bundle break a
|
|
@@ -1622,7 +1715,10 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
|
|
|
1622
1715
|
return task.result()
|
|
1623
1716
|
|
|
1624
1717
|
try:
|
|
1625
|
-
|
|
1718
|
+
try:
|
|
1719
|
+
result = asyncio.run(_go())
|
|
1720
|
+
finally:
|
|
1721
|
+
_progress_stop.set()
|
|
1626
1722
|
except Exception as exc:
|
|
1627
1723
|
_log("turn error:\n" + traceback.format_exc())
|
|
1628
1724
|
_stream["turnId"] = ""
|
package/dist/pythonEnv.js
CHANGED
|
@@ -583,12 +583,26 @@ async function _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress =
|
|
|
583
583
|
// blocked by a uv edge case.
|
|
584
584
|
const _uvBin = _resolveUvBin();
|
|
585
585
|
let acode;
|
|
586
|
+
// Always in the runner log (onProgress is silent outside verbose mode): a deps
|
|
587
|
+
// change reinstalls every runner's venv once, and nobody could tell it happened,
|
|
588
|
+
// how long it took or why it failed (Mac, 2026-10-30).
|
|
589
|
+
const installStart = Date.now();
|
|
590
|
+
const installTail = [];
|
|
591
|
+
const installOut = (line) => {
|
|
592
|
+
installTail.push(line);
|
|
593
|
+
if (installTail.length > 12)
|
|
594
|
+
installTail.shift();
|
|
595
|
+
onProgress(line);
|
|
596
|
+
};
|
|
597
|
+
console.log(` python venv: installing ${PIP_DEPS.length} deps with ${_uvBin ? "uv" : "pip"} (deps changed)`);
|
|
586
598
|
if (_uvBin) {
|
|
587
599
|
onProgress(`installing ${PIP_DEPS.length} python deps via uv (fast; cached reinstalls take seconds)`);
|
|
588
|
-
acode = await runProc(_uvBin, ["pip", "install", "--python", venvPython(), "--quiet", ...PIP_DEPS],
|
|
600
|
+
acode = await runProc(_uvBin, ["pip", "install", "--python", venvPython(), "--quiet", ...PIP_DEPS], installOut, { UV_SYSTEM_CERTS: "1" });
|
|
589
601
|
if (acode !== 0) {
|
|
590
602
|
onProgress("uv install failed — falling back to pip (--no-cache-dir)");
|
|
591
|
-
|
|
603
|
+
console.warn(` python venv: uv failed (exit ${acode}), falling back to pip:\n ${installTail.join("\n ")}`);
|
|
604
|
+
installTail.length = 0;
|
|
605
|
+
acode = await runProc(venvPython(), ["-m", "pip", "install", "--use-feature=truststore", "--no-cache-dir", "--quiet", "--disable-pip-version-check", ...PIP_DEPS], installOut);
|
|
592
606
|
}
|
|
593
607
|
}
|
|
594
608
|
else {
|
|
@@ -599,9 +613,10 @@ async function _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress =
|
|
|
599
613
|
// importantly, guarantees a corrupt cached wheel can never install a subtly
|
|
600
614
|
// broken package into a fresh runner venv. Costs a re-download on rebuild,
|
|
601
615
|
// which is rare (only on a deps change or a manual clear).
|
|
602
|
-
acode = await runProc(venvPython(), ["-m", "pip", "install", "--use-feature=truststore", "--no-cache-dir", "--quiet", "--disable-pip-version-check", ...PIP_DEPS],
|
|
616
|
+
acode = await runProc(venvPython(), ["-m", "pip", "install", "--use-feature=truststore", "--no-cache-dir", "--quiet", "--disable-pip-version-check", ...PIP_DEPS], installOut);
|
|
603
617
|
}
|
|
604
618
|
if (acode !== 0) {
|
|
619
|
+
console.warn(` python venv: install failed (exit ${acode}) after ${Math.round((Date.now() - installStart) / 1000)} s:\n ${installTail.join("\n ")}`);
|
|
605
620
|
return {
|
|
606
621
|
ok: false,
|
|
607
622
|
pythonPath: systemPython,
|
|
@@ -685,5 +700,6 @@ async function _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress =
|
|
|
685
700
|
}
|
|
686
701
|
writeFileSync(VENV_MARK, venvMarkerValue(expectedVersion), "utf-8");
|
|
687
702
|
onProgress(`✓ venv ready (python=${venvPython()})`);
|
|
703
|
+
console.log(` python venv: ready in ${Math.round((Date.now() - installStart) / 1000)} s (python=${venvPython()})`);
|
|
688
704
|
return { ok: true, pythonPath: venvPython() };
|
|
689
705
|
}
|
package/dist/sharedVendor.d.ts
CHANGED
|
@@ -16,3 +16,5 @@
|
|
|
16
16
|
export declare function getLocalSharedVersion(): string | null;
|
|
17
17
|
export declare function getSharedDir(): string;
|
|
18
18
|
export declare function ensureSharedModules(serverUrl: string, expectedVersion: string, runnerToken: string): Promise<void>;
|
|
19
|
+
/** No data for this long aborts the download (a slow one keeps going). */
|
|
20
|
+
export declare const SHARED_STALL_MS = 30000;
|
package/dist/sharedVendor.js
CHANGED
|
@@ -68,6 +68,48 @@ export function ensureSharedModules(serverUrl, expectedVersion, runnerToken) {
|
|
|
68
68
|
_sharedInFlight.set(expectedVersion, p);
|
|
69
69
|
return p;
|
|
70
70
|
}
|
|
71
|
+
/** No data for this long aborts the download (a slow one keeps going). */
|
|
72
|
+
export const SHARED_STALL_MS = 30_000;
|
|
73
|
+
/** Hard ceiling for one download, however slowly it trickles. */
|
|
74
|
+
const SHARED_MAX_MS = 5 * 60_000;
|
|
75
|
+
/** GET the bundle (gzip accepted: about 4 MB instead of 24), aborting only on a stall. */
|
|
76
|
+
async function fetchSharedBundle(url, runnerToken) {
|
|
77
|
+
const ac = new AbortController();
|
|
78
|
+
let stall;
|
|
79
|
+
const arm = () => {
|
|
80
|
+
if (stall)
|
|
81
|
+
clearTimeout(stall);
|
|
82
|
+
stall = setTimeout(() => ac.abort(new Error(`no data for ${SHARED_STALL_MS / 1000} s`)), SHARED_STALL_MS);
|
|
83
|
+
};
|
|
84
|
+
const ceiling = setTimeout(() => ac.abort(new Error(`not done after ${SHARED_MAX_MS / 1000} s`)), SHARED_MAX_MS);
|
|
85
|
+
arm();
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
headers: { Authorization: `Bearer ${runnerToken}`, "Accept-Encoding": "gzip" },
|
|
89
|
+
signal: ac.signal,
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
throw new Error(`Failed to fetch shared modules: HTTP ${res.status}`);
|
|
93
|
+
if (!res.body)
|
|
94
|
+
throw new Error("Failed to fetch shared modules: empty response");
|
|
95
|
+
const reader = res.body.getReader();
|
|
96
|
+
const chunks = [];
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
if (value)
|
|
102
|
+
chunks.push(value);
|
|
103
|
+
arm();
|
|
104
|
+
}
|
|
105
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (stall)
|
|
109
|
+
clearTimeout(stall);
|
|
110
|
+
clearTimeout(ceiling);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
71
113
|
async function _ensureSharedModulesImpl(serverUrl, expectedVersion, runnerToken) {
|
|
72
114
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
73
115
|
// Check cached version. We require BOTH a version match AND a valid nested
|
|
@@ -90,14 +132,26 @@ async function _ensureSharedModulesImpl(serverUrl, expectedVersion, runnerToken)
|
|
|
90
132
|
// Fetch from server
|
|
91
133
|
const httpUrl = serverUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
92
134
|
const url = `${httpUrl}/api/v1/runner/shared-bundle?v=${expectedVersion}`;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
// Right after a deploy every runner downloads the new bundle at once: a slow but
|
|
136
|
+
// live download must not fail the run (run 3751301796504315, 2026-10-30, hit the
|
|
137
|
+
// old flat 30 s limit). Abort only when no data arrives for SHARED_STALL_MS, retry
|
|
138
|
+
// once, and if both fail while a previous bundle is still valid on disk, run with
|
|
139
|
+
// it and say so (the next run fetches the new one).
|
|
140
|
+
let body;
|
|
141
|
+
try {
|
|
142
|
+
body = await fetchSharedBundle(url, runnerToken).catch(async (e) => {
|
|
143
|
+
console.warn(` ⚠ shared modules ${expectedVersion}: download failed (${e?.message}), retrying once`);
|
|
144
|
+
return fetchSharedBundle(url, runnerToken);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
if (existsSync(VERSION_FILE) && cacheLooksValid()) {
|
|
149
|
+
const cached = readFileSync(VERSION_FILE, "utf-8").trim();
|
|
150
|
+
console.warn(` ⚠ shared modules ${expectedVersion}: download failed again (${e?.message}); running with the cached ${cached}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw e;
|
|
99
154
|
}
|
|
100
|
-
const body = await res.json();
|
|
101
155
|
// Verify integrity
|
|
102
156
|
const computed = createHash("sha256").update(JSON.stringify(body.files)).digest("hex");
|
|
103
157
|
if (computed !== body.hash) {
|