@tiens.nguyen/gonext-local-worker 1.0.185 → 1.0.187
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 +62 -7
- package/gonext_agent_chat.py +24 -8
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -36,6 +36,7 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
|
36
36
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
37
37
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
38
38
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
39
|
+
const blue = (s) => `\x1b[34m${s}\x1b[0m`;
|
|
39
40
|
const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
40
41
|
|
|
41
42
|
// The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
|
|
@@ -43,6 +44,15 @@ const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
|
43
44
|
// also drop the worker's ~~~ stream fences (they only matter to the web renderer).
|
|
44
45
|
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
45
46
|
const isFenceLine = (line) => line.trim() === "~~~";
|
|
47
|
+
// The model's raw Python code blob is delimited by <code>/</code> lines. We don't print
|
|
48
|
+
// the tag lines themselves, but the code IN BETWEEN prints in a distinct color (blue)
|
|
49
|
+
// instead of dim prose, so it visually separates from the "Thought:" reasoning.
|
|
50
|
+
const isCodeOpenLine = (line) => line.trim() === "<code>";
|
|
51
|
+
const isCodeCloseLine = (line) => line.trim() === "</code>";
|
|
52
|
+
// The step summary the worker emits right after a tool call finishes (e.g.
|
|
53
|
+
// "unzip_file(...) | → Unzipped 256 files…") duplicates what's already visible in the
|
|
54
|
+
// gonext-local-worker daemon's own terminal — swallow it here to keep the REPL terse.
|
|
55
|
+
const isToolStepSummary = (line) => / \| → /.test(line);
|
|
46
56
|
|
|
47
57
|
// Playful words for the local "thinking…" ticker, reused verbatim from the worker's
|
|
48
58
|
// thinking_words.txt sibling (falls back to a tiny built-in list if it's missing).
|
|
@@ -277,27 +287,65 @@ async function runAgentTurn(history) {
|
|
|
277
287
|
};
|
|
278
288
|
const ticker = setInterval(tick, 1000);
|
|
279
289
|
|
|
280
|
-
// Close out a "thinking…" phase right before real output prints.
|
|
290
|
+
// Close out a "thinking…" phase right before real output prints. A model streams in
|
|
291
|
+
// small bursts with natural micro-pauses between them (token batching, network jitter);
|
|
292
|
+
// each pause >700ms would otherwise stamp its OWN "✔ completed thought" line, stacking
|
|
293
|
+
// many near-duplicates through a single Thought/code generation. `stampIfThinking()` —
|
|
294
|
+
// called ONLY at the <code> tag transition, where nothing prints right after it — HOLDS
|
|
295
|
+
// the stamp as the current status line (like the ticker): a later completion overwrites
|
|
296
|
+
// it in place instead of stacking a new line. `onContent()` — called immediately before
|
|
297
|
+
// any real content prints — always COMMITS whatever is pending (a fresh stamp or a held
|
|
298
|
+
// one) with a trailing newline first, so content never lands on the same line as a stamp.
|
|
299
|
+
let pendingStamp = false;
|
|
300
|
+
const stampIfThinking = () => {
|
|
301
|
+
if (thinking) {
|
|
302
|
+
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`));
|
|
303
|
+
thinking = false;
|
|
304
|
+
statusShown = true;
|
|
305
|
+
pendingStamp = true;
|
|
306
|
+
}
|
|
307
|
+
lastContentAt = Date.now();
|
|
308
|
+
};
|
|
281
309
|
const onContent = () => {
|
|
282
310
|
if (thinking) {
|
|
283
311
|
process.stdout.write(CLEAR_LINE + dim(`✔ completed thought (${thinkSecs()}s)`) + "\n");
|
|
284
312
|
thinking = false;
|
|
285
|
-
|
|
313
|
+
} else if (pendingStamp) {
|
|
314
|
+
process.stdout.write("\n"); // commit the held stamp; content prints right after
|
|
286
315
|
} else {
|
|
287
316
|
clearStatus();
|
|
288
317
|
}
|
|
318
|
+
pendingStamp = false;
|
|
319
|
+
statusShown = false;
|
|
289
320
|
lastContentAt = Date.now();
|
|
290
321
|
};
|
|
291
322
|
|
|
292
|
-
// Split newly-arrived text into lines: heartbeats + ~~~ fences
|
|
293
|
-
//
|
|
323
|
+
// Split newly-arrived text into lines: heartbeats + ~~~ fences + the post-tool-call step
|
|
324
|
+
// summary ("tool(...) | → result", redundant with the daemon's own terminal) are
|
|
325
|
+
// swallowed; the model's <code>/</code> TAG lines are hidden but the code body between
|
|
326
|
+
// them prints in blue (not dim, so it stands out from "Thought:" prose); blank lines
|
|
327
|
+
// keep spacing; everything else commits dim to scrollback.
|
|
328
|
+
let inCode = false;
|
|
294
329
|
const consume = (chunk) => {
|
|
295
330
|
carry += stripThinkTags(chunk);
|
|
296
331
|
let nl;
|
|
297
332
|
while ((nl = carry.indexOf("\n")) >= 0) {
|
|
298
333
|
const line = carry.slice(0, nl);
|
|
299
334
|
carry = carry.slice(nl + 1);
|
|
300
|
-
if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow
|
|
335
|
+
if (isHeartbeatLine(line) || isFenceLine(line)) continue; // swallow, no trace
|
|
336
|
+
if (isToolStepSummary(line)) { lastContentAt = Date.now(); continue; } // swallow
|
|
337
|
+
if (!inCode && isCodeOpenLine(line)) {
|
|
338
|
+
stampIfThinking(); // hold/refresh the stamp; the code body commits it below
|
|
339
|
+
inCode = true;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (inCode) {
|
|
343
|
+
if (isCodeCloseLine(line)) { inCode = false; lastContentAt = Date.now(); continue; }
|
|
344
|
+
if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
|
|
345
|
+
onContent();
|
|
346
|
+
process.stdout.write(blue(line) + "\n");
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
301
349
|
if (line.trim() === "") {
|
|
302
350
|
if (!thinking && !statusShown) process.stdout.write("\n");
|
|
303
351
|
continue;
|
|
@@ -393,10 +441,17 @@ async function main() {
|
|
|
393
441
|
|
|
394
442
|
for (;;) {
|
|
395
443
|
process.stdout.write(cyan(">> "));
|
|
396
|
-
|
|
444
|
+
// Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
|
|
445
|
+
// typed while the agent was busy on a long turn (queued, not yet read) all drain at
|
|
446
|
+
// once the moment the turn ends, each re-printing ">> " with no newline between them
|
|
447
|
+
// (e.g. ">> >> >> "). Only re-print the prompt for a genuine stdin close or input.
|
|
448
|
+
let lineRaw;
|
|
449
|
+
for (;;) {
|
|
450
|
+
lineRaw = await nextLine();
|
|
451
|
+
if (lineRaw === null || lineRaw.trim()) break;
|
|
452
|
+
}
|
|
397
453
|
if (lineRaw === null) break; // stdin closed
|
|
398
454
|
const line = lineRaw.trim();
|
|
399
|
-
if (!line) continue;
|
|
400
455
|
if (line === "/exit" || line === "/quit") break;
|
|
401
456
|
if (line === "/help") {
|
|
402
457
|
console.log(dim(" /exit — quit · /revert [runId] — undo the agent's file edits (latest run)"));
|
package/gonext_agent_chat.py
CHANGED
|
@@ -488,8 +488,14 @@ def _summarise_step(step_log):
|
|
|
488
488
|
name = getattr(tc, "name", "")
|
|
489
489
|
args = getattr(tc, "arguments", None)
|
|
490
490
|
|
|
491
|
-
if name == "python_interpreter"
|
|
492
|
-
code
|
|
491
|
+
if name == "python_interpreter":
|
|
492
|
+
# smolagents' CodeAgent passes the raw code STRING as `arguments` (NOT a
|
|
493
|
+
# dict — only ToolCallingAgent's structured calls use {"code": ...}). Handle
|
|
494
|
+
# both shapes, else this always fell through to the useless "python_interpreter"
|
|
495
|
+
# label below and never showed what was actually called.
|
|
496
|
+
code = args if isinstance(args, str) else (
|
|
497
|
+
args.get("code", "") if isinstance(args, dict) else ""
|
|
498
|
+
)
|
|
493
499
|
# Show the http_request call if present, else first meaningful line
|
|
494
500
|
m = re.search(r'http_request\s*\(\s*(?:method\s*=\s*)?[\'"]?(\w+)[\'"]?\s*,\s*(?:url\s*=\s*)?[\'"]([^\'"]+)', code)
|
|
495
501
|
if m:
|
|
@@ -514,9 +520,13 @@ def _summarise_step(step_log):
|
|
|
514
520
|
|
|
515
521
|
if observations:
|
|
516
522
|
obs = str(observations).strip()
|
|
517
|
-
# smolagents
|
|
518
|
-
#
|
|
519
|
-
|
|
523
|
+
# smolagents wraps observations as "Execution logs:\n<print output>\nLast output
|
|
524
|
+
# from code snippet:\n<return value>" — both header lines are pure boilerplate.
|
|
525
|
+
# Without filtering "Last output from code snippet:" too, a call with no print()
|
|
526
|
+
# output (most of our @tool functions just `return`, not print) surfaced that
|
|
527
|
+
# HEADER as the shown text instead of the actual return value on the next line.
|
|
528
|
+
_OBS_BOILERPLATE = ("Execution logs:", "Last output from code snippet:")
|
|
529
|
+
lines = [l for l in obs.splitlines() if l.strip() and l.strip() not in _OBS_BOILERPLATE]
|
|
520
530
|
if lines:
|
|
521
531
|
parts.append(f"→ {_clip(lines[0])}")
|
|
522
532
|
|
|
@@ -3277,8 +3287,10 @@ def run_agent_chat(cfg):
|
|
|
3277
3287
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
3278
3288
|
dirnames[:] = [d for d in dirnames if d not in _RAG_SKIP_DIRS]
|
|
3279
3289
|
for fn in filenames:
|
|
3280
|
-
|
|
3281
|
-
|
|
3290
|
+
# ABSOLUTE path — read_file_lines/read_text_file resolve relative
|
|
3291
|
+
# paths against the WORKSPACE root, not whatever subfolder was
|
|
3292
|
+
# listed here, so a name relative to `root` could resolve wrong.
|
|
3293
|
+
entries.append(os.path.join(dirpath, fn))
|
|
3282
3294
|
if len(entries) >= 300:
|
|
3283
3295
|
break
|
|
3284
3296
|
if len(entries) >= 300:
|
|
@@ -3447,7 +3459,11 @@ def run_agent_chat(cfg):
|
|
|
3447
3459
|
with open(abs_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
3448
3460
|
for i, line in enumerate(fh, 1):
|
|
3449
3461
|
if rx.search(line):
|
|
3450
|
-
|
|
3462
|
+
# ABSOLUTE path — read_file_lines/read_text_file resolve
|
|
3463
|
+
# relative paths against the WORKSPACE root, not this
|
|
3464
|
+
# search root, so a path relative to `root` (e.g. when
|
|
3465
|
+
# searching a subfolder) would be silently wrong there.
|
|
3466
|
+
hits.append(f"{abs_path}:{i}: {line.rstrip()[:200]}")
|
|
3451
3467
|
if len(hits) >= 100:
|
|
3452
3468
|
break
|
|
3453
3469
|
except OSError:
|
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.187",
|
|
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",
|