@tiens.nguyen/gonext-local-worker 1.0.195 → 1.0.196
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 +19 -7
- package/gonext_agent_chat.py +39 -0
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -36,7 +36,9 @@ 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
|
-
|
|
39
|
+
// ANSI 90 ("bright black") — a reliable neutral grey across terminal themes, unlike
|
|
40
|
+
// code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
|
|
41
|
+
const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
40
42
|
const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
41
43
|
|
|
42
44
|
// The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
|
|
@@ -45,7 +47,7 @@ const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
|
45
47
|
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
46
48
|
const isFenceLine = (line) => line.trim() === "~~~";
|
|
47
49
|
// 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
|
|
50
|
+
// the tag lines themselves, but the code IN BETWEEN prints in a distinct grey (codeColor)
|
|
49
51
|
// instead of dim prose, so it visually separates from the "Thought:" reasoning.
|
|
50
52
|
const isCodeOpenLine = (line) => line.trim() === "<code>";
|
|
51
53
|
const isCodeCloseLine = (line) => line.trim() === "</code>";
|
|
@@ -127,16 +129,26 @@ if (!workerKey || !apiBase) {
|
|
|
127
129
|
// readline stops managing the line/cursor itself; the OS's own canonical-mode terminal
|
|
128
130
|
// still echoes keystrokes normally, and 'line' events fire exactly the same.
|
|
129
131
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
132
|
+
// Side effect of terminal:false above: readline no longer intercepts arrow keys for
|
|
133
|
+
// command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
|
|
134
|
+
// bytes straight through as literal input — the OS's own canonical line discipline
|
|
135
|
+
// doesn't understand them either, so they get echoed and end up IN the submitted line
|
|
136
|
+
// once Enter is pressed (seen live: a task sent to the model started with a literal
|
|
137
|
+
// "\x1b[A" from an accidental up-arrow press). Strip CSI escape sequences (ESC '[' …
|
|
138
|
+
// final-byte) from every submitted line so stray arrow-key presses can't corrupt task
|
|
139
|
+
// text sent to the model.
|
|
140
|
+
const stripAnsiEscapes = (s) => String(s ?? "").replace(/\x1b\[[0-9;]*[A-Za-z~]/g, "");
|
|
130
141
|
const _lineQueue = [];
|
|
131
142
|
let _lineWaiter = null;
|
|
132
143
|
let _stdinClosed = false;
|
|
133
144
|
rl.on("line", (l) => {
|
|
145
|
+
const clean = stripAnsiEscapes(l);
|
|
134
146
|
if (_lineWaiter) {
|
|
135
147
|
const w = _lineWaiter;
|
|
136
148
|
_lineWaiter = null;
|
|
137
|
-
w(
|
|
149
|
+
w(clean);
|
|
138
150
|
} else {
|
|
139
|
-
_lineQueue.push(
|
|
151
|
+
_lineQueue.push(clean);
|
|
140
152
|
}
|
|
141
153
|
});
|
|
142
154
|
rl.on("close", () => {
|
|
@@ -359,8 +371,8 @@ async function runAgentTurn(history) {
|
|
|
359
371
|
// Split newly-arrived text into lines: heartbeats + ~~~ fences + the post-tool-call step
|
|
360
372
|
// summary ("tool(...) | → result", redundant with the daemon's own terminal) are
|
|
361
373
|
// swallowed; the model's <code>/</code> TAG lines are hidden but the code body between
|
|
362
|
-
// them prints in
|
|
363
|
-
// keep spacing; everything else commits dim to scrollback.
|
|
374
|
+
// them prints in a distinct grey (not dim, so it stands out from "Thought:" prose);
|
|
375
|
+
// blank lines keep spacing; everything else commits dim to scrollback.
|
|
364
376
|
let inCode = false;
|
|
365
377
|
let lastWasBlank = true; // suppresses a leading blank line and collapses repeats
|
|
366
378
|
// Chars of the CURRENT trailing (newline-less) `carry` line already flushed live — lets
|
|
@@ -400,7 +412,7 @@ async function runAgentTurn(history) {
|
|
|
400
412
|
if (isCodeCloseLine(line)) { inCode = false; lastContentAt = Date.now(); continue; }
|
|
401
413
|
if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
|
|
402
414
|
onContent();
|
|
403
|
-
process.stdout.write(
|
|
415
|
+
process.stdout.write(codeColor(line) + "\n");
|
|
404
416
|
lastWasBlank = false;
|
|
405
417
|
continue;
|
|
406
418
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -297,6 +297,15 @@ _EMAIL_CONFIRM = re.compile(
|
|
|
297
297
|
re.IGNORECASE,
|
|
298
298
|
)
|
|
299
299
|
|
|
300
|
+
# A bare "continue"-style nudge — matched WHOLE-MESSAGE (anchored) so it only fires on a
|
|
301
|
+
# short, unambiguous continuation cue, never a longer sentence that happens to contain
|
|
302
|
+
# one of these words. See _continuation_pending().
|
|
303
|
+
_CONTINUE_CUE = re.compile(
|
|
304
|
+
r"^(please\s+)?(continue|keep\s+going|go\s+on|resume|carry\s+on|try\s+again)"
|
|
305
|
+
r"[\s.!?]*$",
|
|
306
|
+
re.IGNORECASE,
|
|
307
|
+
)
|
|
308
|
+
|
|
300
309
|
|
|
301
310
|
def _email_allowed(addr, allow):
|
|
302
311
|
"""True if addr matches an allow-list entry — an exact address or a bare domain."""
|
|
@@ -2220,6 +2229,29 @@ def run_agent_chat(cfg):
|
|
|
2220
2229
|
return bool(_EMAIL_AVAILABLE and _EMAIL_CONFIRM.search(latest_user_text or "")
|
|
2221
2230
|
and _prior_email_preview())
|
|
2222
2231
|
|
|
2232
|
+
def _prior_turn_was_interrupted() -> bool:
|
|
2233
|
+
"""True if the immediately preceding assistant turn is one of OUR OWN
|
|
2234
|
+
interrupted-investigation markers (the honest degrade-fallback note, or the
|
|
2235
|
+
max-steps 'ran out of steps' note) — i.e. a genuine tool-use attempt started but
|
|
2236
|
+
didn't finish, as opposed to a normal completed answer."""
|
|
2237
|
+
for m in reversed(messages[:last_user_idx]):
|
|
2238
|
+
if m.get("role") == "assistant":
|
|
2239
|
+
c = m.get("content") or ""
|
|
2240
|
+
return ("ran out of steps before finishing" in c
|
|
2241
|
+
or "couldn't finish the full investigation" in c)
|
|
2242
|
+
return False
|
|
2243
|
+
|
|
2244
|
+
def _continuation_pending() -> bool:
|
|
2245
|
+
"""A bare 'continue'/'keep going' after an interrupted investigation must reach
|
|
2246
|
+
the AGENT, not plain chat — plain chat has zero tool access and can never
|
|
2247
|
+
actually continue a stalled grep_repo/read_file_lines investigation, which is
|
|
2248
|
+
exactly the reported bug: 'continue' silently degraded to a tool-less reply that
|
|
2249
|
+
could only apologize, never resume. Gated tightly (short continuation phrase +
|
|
2250
|
+
genuinely-interrupted prior turn) so it doesn't misfire on an unrelated 'continue'
|
|
2251
|
+
after a normal, already-complete answer."""
|
|
2252
|
+
return bool(_CONTINUE_CUE.match((latest_user_text or "").strip())
|
|
2253
|
+
and _prior_turn_was_interrupted())
|
|
2254
|
+
|
|
2223
2255
|
# Route: does this task need HTTP tool use? A trivial greeting/thank-you/"who are you"
|
|
2224
2256
|
# is caught LOCALLY (no model classifier call, no tool preamble) → straight to a fast
|
|
2225
2257
|
# small-prompt reply. Otherwise ask the model classifier.
|
|
@@ -2236,6 +2268,13 @@ def run_agent_chat(cfg):
|
|
|
2236
2268
|
_log("router → YES (email confirm pending)")
|
|
2237
2269
|
_emit({"type": "step", "text": "→ Agent mode (email confirm)"})
|
|
2238
2270
|
needs_agent = True
|
|
2271
|
+
# A bare "continue" after WE reported an interrupted investigation must reach the
|
|
2272
|
+
# agent too — plain chat has zero tool access, so it can only apologize again, never
|
|
2273
|
+
# actually resume the grep_repo/read_file_lines work (the reported bug).
|
|
2274
|
+
if not needs_agent and _continuation_pending():
|
|
2275
|
+
_log("router → YES (continuation of an interrupted investigation)")
|
|
2276
|
+
_emit({"type": "step", "text": "→ Agent mode (continuing)"})
|
|
2277
|
+
needs_agent = True
|
|
2239
2278
|
|
|
2240
2279
|
if not needs_agent:
|
|
2241
2280
|
_log("router: plain chat (no HTTP needed)")
|
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.196",
|
|
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",
|