@tiens.nguyen/gonext-local-worker 1.0.201 → 1.0.203
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/gonext-repl.mjs +60 -55
- package/gonext_agent_chat.py +25 -6
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -106,6 +106,9 @@ const THINKING_WORDS = (() => {
|
|
|
106
106
|
})();
|
|
107
107
|
const pickWord = () =>
|
|
108
108
|
THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] || "Thinking";
|
|
109
|
+
// Rotating half-circle = an in-progress ("blinking") bullet for the step being worked on.
|
|
110
|
+
const SPINNER = ["◐", "◓", "◑", "◒"];
|
|
111
|
+
const BULLET = "●"; // a completed action
|
|
109
112
|
|
|
110
113
|
// ---------- flags ----------
|
|
111
114
|
const argv = process.argv.slice(2);
|
|
@@ -252,7 +255,10 @@ async function ensureWorkspace() {
|
|
|
252
255
|
const allowRun = await askYesNo(
|
|
253
256
|
"Do you trust this folder — allow the agent to RUN build/test commands\n" +
|
|
254
257
|
"(npm test, mvn, pytest…)?",
|
|
255
|
-
|
|
258
|
+
true /* default Yes — you opened THIS folder with gonext deliberately, and the
|
|
259
|
+
action-first workflows (start/test/build the project) need it; commands are
|
|
260
|
+
still allowlisted (npm/mvn/pytest/…) and run with a scrubbed env. Type n to
|
|
261
|
+
keep a workspace read/edit-only. */
|
|
256
262
|
);
|
|
257
263
|
const next = list.filter((w) => w.path !== cwd);
|
|
258
264
|
next.push({
|
|
@@ -403,17 +409,17 @@ async function runAgentTurn(history) {
|
|
|
403
409
|
// until a full line is known, or we'd risk showing part of something we meant to hide.
|
|
404
410
|
let plainReplyFlow = false;
|
|
405
411
|
|
|
406
|
-
// --- Live "
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
+
// --- Live in-progress ("blinking") bullet -------------------------------------------
|
|
413
|
+
// The agent's raw Thought/code stream is SUPPRESSED (see the ~~~-fence handling in
|
|
414
|
+
// consume) — the terminal shows only a clean list of action bullets. While a step is
|
|
415
|
+
// being thought about / run, we show ONE in-place blinking bullet that counts up —
|
|
416
|
+
// "◐ working… (7s)" — instead of dumping the model's rambling reasoning and stamping a
|
|
417
|
+
// "✔ completed thought" on every token-stream micro-pause (the reported clutter).
|
|
412
418
|
const QUIET_MS = 700; // stream idle this long ⇒ we're waiting on the model
|
|
413
419
|
let lastContentAt = Date.now();
|
|
414
420
|
let thinking = false;
|
|
415
421
|
let thinkingSince = 0;
|
|
416
|
-
let
|
|
422
|
+
let almostDone = false; // set from the worker's "…almost completed thinking…" heartbeat
|
|
417
423
|
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
418
424
|
|
|
419
425
|
const clearStatus = () => {
|
|
@@ -424,52 +430,25 @@ async function runAgentTurn(history) {
|
|
|
424
430
|
if (!following || followAborted || !jobRunning) return;
|
|
425
431
|
if (Date.now() - lastContentAt < QUIET_MS) return; // tokens still flowing
|
|
426
432
|
// A plain-reply answer streams onto a line with NO trailing newline until it's fully
|
|
427
|
-
// done —
|
|
428
|
-
//
|
|
429
|
-
// CURRENT visual row, not the whole logical line once it's wrapped — overwriting the
|
|
430
|
-
// ticker there corrupts the visible answer text mid-word. Stay silent once any of
|
|
431
|
-
// THIS line has already been shown; the next completed line (fresh, empty) is safe.
|
|
433
|
+
// done — overwriting the ticker there corrupts the visible answer mid-word. Stay
|
|
434
|
+
// silent once any of THIS line has already been shown (see carryFlushedLen).
|
|
432
435
|
if (plainReplyFlow && carryFlushedLen > 0) return;
|
|
433
|
-
// Count from when the stream
|
|
434
|
-
|
|
435
|
-
if (!thinking) { thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); }
|
|
436
|
+
// Count from when the stream went quiet, so the seconds reflect the true wait.
|
|
437
|
+
if (!thinking) { thinking = true; thinkingSince = lastContentAt; }
|
|
436
438
|
const secs = thinkSecs();
|
|
437
|
-
|
|
438
|
-
|
|
439
|
+
const glyph = SPINNER[secs % SPINNER.length]; // rotates → reads as "in progress"
|
|
440
|
+
const label = almostDone ? "almost done" : "working";
|
|
441
|
+
process.stdout.write(CLEAR_LINE + yellow(`${glyph} ${label}… (${secs}s)`));
|
|
439
442
|
statusShown = true;
|
|
440
443
|
};
|
|
441
444
|
const ticker = setInterval(tick, 1000);
|
|
442
445
|
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// many near-duplicates through a single Thought/code generation. `stampIfThinking()` —
|
|
447
|
-
// called ONLY at the <code> tag transition, where nothing prints right after it — HOLDS
|
|
448
|
-
// the stamp as the current status line (like the ticker): a later completion overwrites
|
|
449
|
-
// it in place instead of stacking a new line. `onContent()` — called immediately before
|
|
450
|
-
// any real content prints — always COMMITS whatever is pending (a fresh stamp or a held
|
|
451
|
-
// one) with a trailing newline first, so content never lands on the same line as a stamp.
|
|
452
|
-
let pendingStamp = false;
|
|
453
|
-
const stampIfThinking = () => {
|
|
454
|
-
if (thinking) {
|
|
455
|
-
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`));
|
|
456
|
-
thinking = false;
|
|
457
|
-
statusShown = true;
|
|
458
|
-
pendingStamp = true;
|
|
459
|
-
}
|
|
460
|
-
lastContentAt = Date.now();
|
|
461
|
-
};
|
|
446
|
+
// Called right before any real (bullet/edit-card/answer) content prints: drop the
|
|
447
|
+
// blinking-bullet status line so content lands cleanly on its own line. No "completed
|
|
448
|
+
// thought" stamp — each finished action prints its own solid ● bullet instead.
|
|
462
449
|
const onContent = () => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
thinking = false;
|
|
466
|
-
} else if (pendingStamp) {
|
|
467
|
-
process.stdout.write("\n"); // commit the held stamp; content prints right after
|
|
468
|
-
} else {
|
|
469
|
-
clearStatus();
|
|
470
|
-
}
|
|
471
|
-
pendingStamp = false;
|
|
472
|
-
statusShown = false;
|
|
450
|
+
clearStatus();
|
|
451
|
+
if (thinking) { process.stdout.write(CLEAR_LINE); thinking = false; }
|
|
473
452
|
lastContentAt = Date.now();
|
|
474
453
|
};
|
|
475
454
|
|
|
@@ -480,6 +459,12 @@ async function runAgentTurn(history) {
|
|
|
480
459
|
// blank lines keep spacing; everything else commits dim to scrollback.
|
|
481
460
|
let inCode = false;
|
|
482
461
|
let inEditCard = false; // inside a colored edit-card block (see isEditCardHeader)
|
|
462
|
+
// The worker wraps the raw model token stream (Thought prose + code) in ~~~ fences;
|
|
463
|
+
// the concrete ACTION events (tool steps, edit cards) are emitted OUTSIDE them. We
|
|
464
|
+
// suppress everything IN a fence — the rambling Thought and redundant raw code (the
|
|
465
|
+
// edit card already shows real changes) — and render the out-of-fence actions as
|
|
466
|
+
// bullets. This is the whole "action bullets only" declutter (task #41).
|
|
467
|
+
let inStreamFence = false;
|
|
483
468
|
let lastWasBlank = true; // suppresses a leading blank line and collapses repeats
|
|
484
469
|
// Chars of the CURRENT trailing (newline-less) `carry` line already flushed live — lets
|
|
485
470
|
// a long plain-reply answer stream character-by-character instead of waiting for a "\n"
|
|
@@ -493,7 +478,20 @@ async function runAgentTurn(history) {
|
|
|
493
478
|
carry = carry.slice(nl + 1);
|
|
494
479
|
const alreadyFlushed = carryFlushedLen;
|
|
495
480
|
carryFlushedLen = 0; // reset — `carry` now starts a fresh pending line
|
|
496
|
-
if (isHeartbeatLine(line)
|
|
481
|
+
if (isHeartbeatLine(line)) {
|
|
482
|
+
// Heartbeats are swallowed (the local ticker drives the display), but they
|
|
483
|
+
// still carry ONE bit we want: once the worker's word is "…almost completed
|
|
484
|
+
// thinking…", this model call is past prompt-eval and producing output — flip
|
|
485
|
+
// the local ticker to the same wording. A plain random-word heartbeat means
|
|
486
|
+
// we're back to waiting (a fresh step's prompt-eval), so clear the flag.
|
|
487
|
+
almostDone = /almost/i.test(line);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (isFenceLine(line)) { inStreamFence = !inStreamFence; continue; }
|
|
491
|
+
// Inside a ~~~ fence = raw model Thought/code stream → suppressed entirely. Do NOT
|
|
492
|
+
// bump lastContentAt: while the (now hidden) tokens flow, the blinking bullet keeps
|
|
493
|
+
// ticking so the user sees the step is still being worked on.
|
|
494
|
+
if (inStreamFence) continue;
|
|
497
495
|
if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
|
|
498
496
|
if (isRoutingLine(line)) {
|
|
499
497
|
// Replace the whole routing play-by-play with ONE friendly line, shown once
|
|
@@ -544,7 +542,8 @@ async function runAgentTurn(history) {
|
|
|
544
542
|
if (openM) {
|
|
545
543
|
// Enter code mode (or stay in it on a duplicate tag). Anything glued after
|
|
546
544
|
// the tag on the same line is code — print it, minus a glued close tag.
|
|
547
|
-
|
|
545
|
+
// (Reached only for a stray <code> OUTSIDE a ~~~ fence; in-fence code is
|
|
546
|
+
// suppressed above. Kept as a safety net.)
|
|
548
547
|
inCode = true;
|
|
549
548
|
let rest = t.slice(openM[0].length).trim();
|
|
550
549
|
if (CODE_CLOSE_RE.test(rest)) {
|
|
@@ -595,12 +594,18 @@ async function runAgentTurn(history) {
|
|
|
595
594
|
continue;
|
|
596
595
|
}
|
|
597
596
|
if (alreadyFlushed === 0) onContent();
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
597
|
+
if (plainReplyFlow) {
|
|
598
|
+
// Plain-reply content IS the answer — print it in the default color and remember
|
|
599
|
+
// it's visible so the caller doesn't reprint it once the turn completes.
|
|
600
|
+
process.stdout.write(line.slice(alreadyFlushed) + "\n");
|
|
601
|
+
answerShownLive = true;
|
|
602
|
+
} else {
|
|
603
|
+
// Agent flow: with the raw Thought/code stream suppressed above, the only lines
|
|
604
|
+
// that reach here are the worker's out-of-fence ACTION events ("Running → npx…",
|
|
605
|
+
// "Searching code → import (11 hits)", "Server up → …", "Composing answer…").
|
|
606
|
+
// Render each as ONE solid bullet — the clean, summarized action list (task #41).
|
|
607
|
+
process.stdout.write(green(BULLET) + " " + line.trim() + "\n");
|
|
608
|
+
}
|
|
604
609
|
lastWasBlank = false;
|
|
605
610
|
}
|
|
606
611
|
// Plain-reply answers often have NO internal newline at all (a short one-sentence
|
package/gonext_agent_chat.py
CHANGED
|
@@ -42,6 +42,13 @@ _REAL_STDOUT = sys.stdout
|
|
|
42
42
|
# a random one is picked per tick so the wait feels alive instead of a fixed string.
|
|
43
43
|
_THINKING_WORDS: list = []
|
|
44
44
|
|
|
45
|
+
# Shown INSTEAD of a random word once the model's first token arrives (prompt-eval done,
|
|
46
|
+
# tokens now flowing) — the long silent wait is over and output is being produced, so the
|
|
47
|
+
# status flips from "still thinking" to "almost done". Ends with "…" so the REPL/web can
|
|
48
|
+
# append "(Ns)". The REPL detects the word "almost" in a heartbeat line to switch its own
|
|
49
|
+
# local ticker to the same wording (see gonext-repl.mjs).
|
|
50
|
+
_STATUS_ALMOST_DONE = "…almost completed thinking…"
|
|
51
|
+
|
|
45
52
|
|
|
46
53
|
def _thinking_word() -> str:
|
|
47
54
|
"""A random playful status word (e.g. 'Caffeinating'). Falls back to a small
|
|
@@ -3390,8 +3397,15 @@ def run_agent_chat(cfg):
|
|
|
3390
3397
|
nonlocal first_token_at
|
|
3391
3398
|
if first_token_at is None:
|
|
3392
3399
|
first_token_at = time.monotonic()
|
|
3393
|
-
|
|
3400
|
+
elapsed = first_token_at - t0
|
|
3401
|
+
_log(f"streamed generate: FIRST token after {elapsed:.1f}s "
|
|
3394
3402
|
"(prompt-eval done; tokens now flowing)")
|
|
3403
|
+
# Flip the status from "still thinking" to "almost done" the instant
|
|
3404
|
+
# the wait ends — the heartbeat below keeps it there for the rest of
|
|
3405
|
+
# this call (self._first_token_seen), and this one-shot emit shows it
|
|
3406
|
+
# immediately (before the next 45s heartbeat would).
|
|
3407
|
+
self._first_token_seen = True
|
|
3408
|
+
_emit({"type": "step", "text": f"{_STATUS_ALMOST_DONE} ({elapsed:.0f}s)"})
|
|
3395
3409
|
# Separate this step's live thinking from the previous step summary.
|
|
3396
3410
|
_emit({"type": "stream", "text": "\n"})
|
|
3397
3411
|
|
|
@@ -3531,16 +3545,21 @@ def run_agent_chat(cfg):
|
|
|
3531
3545
|
# 31B on a remote Ollama box) can take minutes on prompt-eval before the
|
|
3532
3546
|
# first byte — emit a keepalive step every 45s until the call returns.
|
|
3533
3547
|
hb_stop = threading.Event()
|
|
3548
|
+
# Reset per call: True once this call's first token arrives (set in
|
|
3549
|
+
# _mark_first), read by the heartbeat below to switch its wording.
|
|
3550
|
+
self._first_token_seen = False
|
|
3534
3551
|
|
|
3535
3552
|
def _heartbeat():
|
|
3536
3553
|
waited = 0
|
|
3537
3554
|
while not hb_stop.wait(45):
|
|
3538
3555
|
waited += 45
|
|
3539
|
-
#
|
|
3540
|
-
#
|
|
3541
|
-
#
|
|
3542
|
-
|
|
3543
|
-
|
|
3556
|
+
# Before the first token: a random playful word ("Caffeinating… (90s)")
|
|
3557
|
+
# — a large/cold model can take minutes on prompt-eval. After the first
|
|
3558
|
+
# token: "…almost completed thinking… (Ns)", since output is now flowing
|
|
3559
|
+
# and the call is in its finishing phase, not stuck waiting.
|
|
3560
|
+
word = (_STATUS_ALMOST_DONE if getattr(self, "_first_token_seen", False)
|
|
3561
|
+
else f"{_thinking_word()}…")
|
|
3562
|
+
_emit({"type": "step", "text": f"{word} ({waited}s)"})
|
|
3544
3563
|
_log(f"heartbeat: model call in flight {waited}s")
|
|
3545
3564
|
|
|
3546
3565
|
hb = threading.Thread(target=_heartbeat, daemon=True)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.203",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|