@tiens.nguyen/gonext-local-worker 1.0.180 → 1.0.181
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 +77 -24
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Commands: /exit /revert [runId] /help Ctrl-C stops following the current run.
|
|
20
20
|
*/
|
|
21
21
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
22
|
-
import { existsSync, statSync } from "node:fs";
|
|
22
|
+
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { dirname, join, resolve, basename } from "node:path";
|
|
@@ -38,11 +38,26 @@ const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
|
38
38
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
39
39
|
const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
40
40
|
|
|
41
|
-
// The worker's
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// and that gets erased once real output arrives, so they never pile up in the scrollback.
|
|
41
|
+
// The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
|
|
42
|
+
// SWALLOW them from the terminal (the local ticker below drives progress instead), and
|
|
43
|
+
// also drop the worker's ~~~ stream fences (they only matter to the web renderer).
|
|
45
44
|
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
45
|
+
const isFenceLine = (line) => line.trim() === "~~~";
|
|
46
|
+
|
|
47
|
+
// Playful words for the local "thinking…" ticker, reused verbatim from the worker's
|
|
48
|
+
// thinking_words.txt sibling (falls back to a tiny built-in list if it's missing).
|
|
49
|
+
const THINKING_WORDS = (() => {
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(join(DIR, "thinking_words.txt"), "utf8")
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((l) => l.trim())
|
|
54
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
55
|
+
} catch {
|
|
56
|
+
return ["Thinking", "Percolating", "Cogitating", "Noodling", "Ruminating"];
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
const pickWord = () =>
|
|
60
|
+
THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] || "Thinking";
|
|
46
61
|
|
|
47
62
|
// ---------- flags ----------
|
|
48
63
|
const argv = process.argv.slice(2);
|
|
@@ -227,38 +242,74 @@ async function runAgentTurn(history) {
|
|
|
227
242
|
followAborted = false;
|
|
228
243
|
const startedAt = Date.now();
|
|
229
244
|
let shownChars = 0;
|
|
230
|
-
let carry = ""; // trailing partial line held until its newline arrives
|
|
231
|
-
let statusShown = false; // a transient
|
|
245
|
+
let carry = ""; // trailing partial content line held until its newline arrives
|
|
246
|
+
let statusShown = false; // a transient status line is currently on screen
|
|
232
247
|
let warnedPending = false;
|
|
248
|
+
let jobRunning = false; // only tick "thinking…" once the worker has claimed the job
|
|
249
|
+
|
|
250
|
+
// --- Live "thinking…" ticker ---------------------------------------------------
|
|
251
|
+
// Between visible tokens (prompt-eval / awaiting the first token) we show ONE in-place
|
|
252
|
+
// status line that counts UP every second — "Percolating thinking… (7s)" — so the wait
|
|
253
|
+
// feels alive. When output resumes we stamp a dim "✔ completed thought (Ns)" and let the
|
|
254
|
+
// tokens stream. The worker's own 45s heartbeat lines (and ~~~ fences) are swallowed;
|
|
255
|
+
// this local 1-second ticker owns the progress display.
|
|
256
|
+
const QUIET_MS = 700; // stream idle this long ⇒ we're waiting on the model
|
|
257
|
+
let lastContentAt = Date.now();
|
|
258
|
+
let thinking = false;
|
|
259
|
+
let thinkingSince = 0;
|
|
260
|
+
let thinkWord = pickWord();
|
|
261
|
+
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
233
262
|
|
|
234
|
-
// Erase the transient heartbeat status line (if any) before printing real output.
|
|
235
263
|
const clearStatus = () => {
|
|
236
|
-
if (statusShown) {
|
|
237
|
-
|
|
264
|
+
if (statusShown) { process.stdout.write(CLEAR_LINE); statusShown = false; }
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const tick = () => {
|
|
268
|
+
if (!following || followAborted || !jobRunning) return;
|
|
269
|
+
if (Date.now() - lastContentAt < QUIET_MS) return; // tokens still flowing
|
|
270
|
+
// Count from when the stream actually went quiet, so the seconds reflect the true
|
|
271
|
+
// wait for the first token (1s, 2s, 3s…) rather than from the first tick.
|
|
272
|
+
if (!thinking) { thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); }
|
|
273
|
+
const secs = thinkSecs();
|
|
274
|
+
if (secs % 12 === 0) thinkWord = pickWord(); // rotate the word on long waits
|
|
275
|
+
process.stdout.write(CLEAR_LINE + yellow(`${thinkWord} thinking… (${secs}s)`));
|
|
276
|
+
statusShown = true;
|
|
277
|
+
};
|
|
278
|
+
const ticker = setInterval(tick, 1000);
|
|
279
|
+
|
|
280
|
+
// Close out a "thinking…" phase right before real output prints.
|
|
281
|
+
const onContent = () => {
|
|
282
|
+
if (thinking) {
|
|
283
|
+
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`) + "\n");
|
|
284
|
+
thinking = false;
|
|
238
285
|
statusShown = false;
|
|
286
|
+
} else {
|
|
287
|
+
clearStatus();
|
|
239
288
|
}
|
|
289
|
+
lastContentAt = Date.now();
|
|
240
290
|
};
|
|
241
|
-
|
|
242
|
-
//
|
|
291
|
+
|
|
292
|
+
// Split newly-arrived text into lines: heartbeats + ~~~ fences are swallowed (the ticker
|
|
293
|
+
// shows progress); blank lines keep spacing; everything else commits dim to scrollback.
|
|
243
294
|
const consume = (chunk) => {
|
|
244
295
|
carry += stripThinkTags(chunk);
|
|
245
296
|
let nl;
|
|
246
297
|
while ((nl = carry.indexOf("\n")) >= 0) {
|
|
247
298
|
const line = carry.slice(0, nl);
|
|
248
299
|
carry = carry.slice(nl + 1);
|
|
249
|
-
if (isHeartbeatLine(line))
|
|
250
|
-
|
|
251
|
-
process.stdout.write(
|
|
252
|
-
|
|
253
|
-
} else if (line.trim() === "") {
|
|
254
|
-
// Swallow the blank line the worker appends after a heartbeat; keep real spacing.
|
|
255
|
-
if (!statusShown) process.stdout.write("\n");
|
|
256
|
-
} else {
|
|
257
|
-
clearStatus();
|
|
258
|
-
process.stdout.write(dim(line) + "\n");
|
|
300
|
+
if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow
|
|
301
|
+
if (line.trim() === "") {
|
|
302
|
+
if (!thinking && !statusShown) process.stdout.write("\n");
|
|
303
|
+
continue;
|
|
259
304
|
}
|
|
305
|
+
onContent();
|
|
306
|
+
process.stdout.write(dim(line) + "\n");
|
|
260
307
|
}
|
|
308
|
+
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
309
|
+
// a newline), so tokens are actively flowing — keep the ticker asleep.
|
|
310
|
+
if (carry.trim() !== "") lastContentAt = Date.now();
|
|
261
311
|
};
|
|
312
|
+
|
|
262
313
|
try {
|
|
263
314
|
for (;;) {
|
|
264
315
|
if (followAborted) {
|
|
@@ -273,10 +324,11 @@ async function runAgentTurn(history) {
|
|
|
273
324
|
});
|
|
274
325
|
const job = await jr.json().catch(() => ({}));
|
|
275
326
|
if (!jr.ok) throw new Error(job?.error || `job poll failed (HTTP ${jr.status})`);
|
|
327
|
+
jobRunning = job.jobStatus === "running";
|
|
276
328
|
const text = typeof job.resultText === "string" ? job.resultText : "";
|
|
277
329
|
if (job.jobStatus === "completed") {
|
|
278
|
-
// Drop the transient
|
|
279
|
-
//
|
|
330
|
+
// Drop the transient status line and any partial reasoning tail; the bright answer
|
|
331
|
+
// prints right after via the caller.
|
|
280
332
|
clearStatus();
|
|
281
333
|
return answerFrom(text);
|
|
282
334
|
}
|
|
@@ -302,6 +354,7 @@ async function runAgentTurn(history) {
|
|
|
302
354
|
await sleep(700);
|
|
303
355
|
}
|
|
304
356
|
} finally {
|
|
357
|
+
clearInterval(ticker);
|
|
305
358
|
clearStatus();
|
|
306
359
|
following = false;
|
|
307
360
|
}
|
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.181",
|
|
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",
|