@melaya/runner 1.1.36 → 1.1.38
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 +45 -13
- package/dist/connection.js +30 -4
- package/dist/pythonEnv.d.ts +4 -2
- package/dist/pythonEnv.js +18 -1
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -184,7 +184,16 @@ def _render_summary_text(summary) -> str:
|
|
|
184
184
|
# Streaming state for the CURRENT turn — the pre_print / post_acting hooks read
|
|
185
185
|
# this to emit delta / tool events keyed to the turn in flight. `cancel` is the
|
|
186
186
|
# STOP flag: set by the stdin-reader thread, polled by the running turn.
|
|
187
|
-
_stream = {"turnId": "", "lens": {}, "cancel": False, "usedBrowser": False
|
|
187
|
+
_stream = {"turnId": "", "lens": {}, "cancel": False, "usedBrowser": False,
|
|
188
|
+
"deltaBuf": "", "deltaMid": ""}
|
|
189
|
+
|
|
190
|
+
# Coalesce per-token pre_print fragments before emitting a `delta`. agentscope
|
|
191
|
+
# hands us one cumulative snapshot per model chunk; a streaming Anthropic/OpenAI
|
|
192
|
+
# response fragments the answer into 2-15 char pieces, and emitting a MELASSIST
|
|
193
|
+
# line (→ socket → server → SSE → client) PER fragment made streaming read as
|
|
194
|
+
# ultra-choppy / ultra-slow (hundreds of round-trips for one answer). Hold the
|
|
195
|
+
# suffix until it reaches ~a clause or hits a line break, then flush one delta.
|
|
196
|
+
_DELTA_FLUSH_CHARS = 48
|
|
188
197
|
|
|
189
198
|
# Sentinel returned by a turn's coroutine when the user pressed STOP.
|
|
190
199
|
_CANCELLED = object()
|
|
@@ -417,6 +426,18 @@ def _emit(turn_id: str, kind: str, **fields) -> None:
|
|
|
417
426
|
pass
|
|
418
427
|
|
|
419
428
|
|
|
429
|
+
def _flush_delta(turn_id: str) -> None:
|
|
430
|
+
"""Emit any buffered pre_print suffix as one `delta` and clear the buffer.
|
|
431
|
+
|
|
432
|
+
Called from the pre_print hook when the buffer fills / a message boundary is
|
|
433
|
+
crossed, and from _run_turn just before the authoritative `text` swap so the
|
|
434
|
+
last partial clause is not left un-streamed."""
|
|
435
|
+
buf = _stream.get("deltaBuf") or ""
|
|
436
|
+
if buf and turn_id:
|
|
437
|
+
_emit(turn_id, "delta", content=buf)
|
|
438
|
+
_stream["deltaBuf"] = ""
|
|
439
|
+
|
|
440
|
+
|
|
420
441
|
def _log(msg: str) -> None:
|
|
421
442
|
# Diagnostics go to stderr (the runner logs stderr; stdout is the event channel).
|
|
422
443
|
try:
|
|
@@ -826,19 +847,27 @@ def _register_stream_hooks(agent) -> None:
|
|
|
826
847
|
return
|
|
827
848
|
prev = _stream["lens"].get(mid, 0)
|
|
828
849
|
if len(text) > prev:
|
|
829
|
-
# ReAct emits one message PER iteration
|
|
830
|
-
#
|
|
831
|
-
#
|
|
832
|
-
#
|
|
833
|
-
#
|
|
834
|
-
#
|
|
835
|
-
#
|
|
836
|
-
#
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
#
|
|
840
|
-
|
|
850
|
+
# ReAct emits one message PER iteration. We stream every message's
|
|
851
|
+
# growing suffix into the answer bubble as `delta` so the reply
|
|
852
|
+
# TYPES OUT live (this is the same channel cloud providers stream
|
|
853
|
+
# on) instead of only landing whole at end-of-turn. The end-of-turn
|
|
854
|
+
# `text` event (_run_turn) still swaps in the clean, authoritative
|
|
855
|
+
# markdown answer, so transient tool preambles that streamed here
|
|
856
|
+
# get replaced by the final professional reply. A blank line
|
|
857
|
+
# separates successive ReAct messages so rounds don't run together
|
|
858
|
+
# ("...page content.I notice..."). Fragments are coalesced (see
|
|
859
|
+
# _DELTA_FLUSH_CHARS) so we emit clause-sized deltas, not one
|
|
860
|
+
# socket round-trip per token.
|
|
861
|
+
suffix = text[prev:]
|
|
841
862
|
_stream["lens"][mid] = len(text)
|
|
863
|
+
if mid != _stream.get("deltaMid"):
|
|
864
|
+
_flush_delta(tid) # flush the previous message's tail
|
|
865
|
+
if _stream.get("deltaMid"):
|
|
866
|
+
_stream["deltaBuf"] = "\n\n"
|
|
867
|
+
_stream["deltaMid"] = mid
|
|
868
|
+
_stream["deltaBuf"] = _stream.get("deltaBuf", "") + suffix
|
|
869
|
+
if len(_stream["deltaBuf"]) >= _DELTA_FLUSH_CHARS or "\n" in suffix:
|
|
870
|
+
_flush_delta(tid)
|
|
842
871
|
except Exception:
|
|
843
872
|
pass
|
|
844
873
|
|
|
@@ -921,6 +950,8 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
|
|
|
921
950
|
_emit(turn_id, "round", n=1)
|
|
922
951
|
_stream["turnId"] = turn_id
|
|
923
952
|
_stream["lens"] = {}
|
|
953
|
+
_stream["deltaBuf"] = "" # fresh turn — drop any stale streamed suffix
|
|
954
|
+
_stream["deltaMid"] = ""
|
|
924
955
|
_stream["cancel"] = False # fresh turn — clear any stale STOP
|
|
925
956
|
_stream["usedBrowser"] = False
|
|
926
957
|
|
|
@@ -993,6 +1024,7 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
|
|
|
993
1024
|
_emit(turn_id, "done")
|
|
994
1025
|
return
|
|
995
1026
|
|
|
1027
|
+
_flush_delta(turn_id) # push the last buffered clause before the swap
|
|
996
1028
|
_stream["turnId"] = ""
|
|
997
1029
|
# Authoritative final answer — the client swaps the streamed plain text for
|
|
998
1030
|
# this markdown-rendered version.
|
package/dist/connection.js
CHANGED
|
@@ -136,14 +136,40 @@ export async function connect(opts) {
|
|
|
136
136
|
_assistantPrewarmed = true;
|
|
137
137
|
try {
|
|
138
138
|
const { getLocalSharedVersion } = await import("./sharedVendor.js");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
let localVersion = getLocalSharedVersion();
|
|
140
|
+
const freshSetup = !localVersion;
|
|
141
|
+
if (!localVersion) {
|
|
142
|
+
// FIRST LAUNCH: no shared bundle on disk yet. Download it now, on
|
|
143
|
+
// connect, so the venv is built UP-FRONT instead of making the user wait
|
|
144
|
+
// 1-2 min mid-chat on their first assistant/pipeline run. Shown to the
|
|
145
|
+
// user (not verbose-gated) — a silent multi-minute pause reads as "stuck"
|
|
146
|
+
// and users leave; a clear one-time-setup line keeps their trust.
|
|
147
|
+
console.log(chalk.hex("#7C6FF0")("\n ⚙ First-time setup: preparing your local AI runtime (one-time)…"));
|
|
148
|
+
console.log(chalk.gray(" downloading runtime modules…"));
|
|
149
|
+
try {
|
|
150
|
+
await ensureSharedModules(opts.serverUrl, "latest", opts.token);
|
|
151
|
+
localVersion = getLocalSharedVersion();
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
console.log(chalk.gray(` setup deferred (${e?.message || e}) — it will finish on your first run`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!localVersion)
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
142
160
|
const { ensurePythonEnv, getCertBundlePath } = await import("./pythonEnv.js");
|
|
161
|
+
if (freshSetup)
|
|
162
|
+
console.log(chalk.gray(" installing Python dependencies (~1-2 min)…"));
|
|
143
163
|
const envResult = await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
|
|
144
164
|
console.log(chalk.gray(` [assistant prewarm] ${m}`)); });
|
|
145
|
-
if (
|
|
165
|
+
if (freshSetup) {
|
|
166
|
+
console.log(envResult.ok
|
|
167
|
+
? chalk.green(" ✓ Local AI runtime ready — your first chat will start instantly\n")
|
|
168
|
+
: chalk.yellow(` ⚠ runtime setup did not finish (${envResult.reason || "unknown"}); it will retry on your first run\n`));
|
|
169
|
+
}
|
|
170
|
+
else if (opts.verbose) {
|
|
146
171
|
console.log(chalk.gray(" [assistant prewarm] ready"));
|
|
172
|
+
}
|
|
147
173
|
// ALSO pre-import the heavy Python stack so the first real run/chat
|
|
148
174
|
// doesn't pay the ~6s cold `import shared.runtime.registry`. Spawn it
|
|
149
175
|
// detached + fire-and-forget with the SAME PYTHONPATH/env a real
|
package/dist/pythonEnv.d.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function venvPython(): string;
|
|
13
13
|
export declare function getCertBundlePath(): string;
|
|
14
|
-
|
|
14
|
+
type EnsureResult = {
|
|
15
15
|
ok: boolean;
|
|
16
16
|
pythonPath: string;
|
|
17
17
|
reason?: string;
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
|
+
export declare function ensurePythonEnv(systemPython: string, expectedVersion: string, onProgress?: (msg: string) => void): Promise<EnsureResult>;
|
|
20
|
+
export {};
|
package/dist/pythonEnv.js
CHANGED
|
@@ -389,7 +389,24 @@ async function ensureNltkData(onProgress) {
|
|
|
389
389
|
}
|
|
390
390
|
// One-time-per-process guard for the cache-hit self-heal (NLTK + cert bundle).
|
|
391
391
|
let _selfHealedThisProcess = false;
|
|
392
|
-
|
|
392
|
+
const _ensureInFlight = new Map();
|
|
393
|
+
export function ensurePythonEnv(systemPython, expectedVersion, onProgress = (m) => console.log(chalk.gray(` [venv] ${m}`))) {
|
|
394
|
+
// A valid venv resolves cheaply and must NOT be blocked behind an in-flight
|
|
395
|
+
// build for a different version — run the impl directly (its own early-return
|
|
396
|
+
// handles the cache hit). Only route through the singleflight when a build is
|
|
397
|
+
// actually needed, so two build requests coalesce.
|
|
398
|
+
if (venvIsValid(expectedVersion) && !_ensureInFlight.has(expectedVersion)) {
|
|
399
|
+
return _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress);
|
|
400
|
+
}
|
|
401
|
+
const existing = _ensureInFlight.get(expectedVersion);
|
|
402
|
+
if (existing)
|
|
403
|
+
return existing;
|
|
404
|
+
const p = _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress)
|
|
405
|
+
.finally(() => { _ensureInFlight.delete(expectedVersion); });
|
|
406
|
+
_ensureInFlight.set(expectedVersion, p);
|
|
407
|
+
return p;
|
|
408
|
+
}
|
|
409
|
+
async function _ensurePythonEnvImpl(systemPython, expectedVersion, onProgress = (m) => console.log(chalk.gray(` [venv] ${m}`))) {
|
|
393
410
|
if (venvIsValid(expectedVersion)) {
|
|
394
411
|
// Self-heal: ensure NLTK data + the cert bundle are present even when the
|
|
395
412
|
// venv marker says we're up to date (this is what previously broke for
|