@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 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
- statusShown = false;
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 are swallowed (the ticker
293
- // shows progress); blank lines keep spacing; everything else commits dim to scrollback.
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
- const lineRaw = await nextLine();
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)"));
@@ -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" and isinstance(args, dict):
492
- code = args.get("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 prefixes output with "Execution logs:" skip that header line
518
- # and show the first line of actual content.
519
- lines = [l for l in obs.splitlines() if l.strip() and l.strip() != "Execution logs:"]
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
- rel = os.path.relpath(os.path.join(dirpath, fn), root)
3281
- entries.append(rel)
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
- hits.append(f"{rel}:{i}: {line.rstrip()[:200]}")
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.185",
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",