@tiens.nguyen/gonext-local-worker 1.0.198 → 1.0.200
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 +84 -28
- package/gonext_agent_chat.py +53 -6
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -54,11 +54,16 @@ const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
|
54
54
|
// also drop the worker's ~~~ stream fences (they only matter to the web renderer).
|
|
55
55
|
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
56
56
|
const isFenceLine = (line) => line.trim() === "~~~";
|
|
57
|
-
// The model's raw Python code blob is delimited by <code>/</code>
|
|
58
|
-
// the
|
|
57
|
+
// The model's raw Python code blob is delimited by <code>/</code> tags. We don't print
|
|
58
|
+
// the tags themselves, but the code IN BETWEEN prints in a distinct grey (codeColor)
|
|
59
59
|
// instead of dim prose, so it visually separates from the "Thought:" reasoning.
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Tolerant matching, NOT exact-line: the raw token stream often mangles the tags —
|
|
61
|
+
// glued to content ("<codecreate_folder(...)"), missing the ">", duplicated, or the
|
|
62
|
+
// close tag stuck on the code's last line — and exact matching printed those
|
|
63
|
+
// fragments literally in mixed colors (user-reported).
|
|
64
|
+
const CODE_OPEN_RE = /^<code(?:\s*>)?/; // applied to line.trim()
|
|
65
|
+
const CODE_CLOSE_RE = /<\/code>?\s*$/;
|
|
66
|
+
const isBareCloseTag = (t) => /^<\/code>?$/.test(t);
|
|
62
67
|
// The step summary the worker emits right after a tool call finishes (e.g.
|
|
63
68
|
// "unzip_file(...) | → Unzipped 256 files…") duplicates what's already visible in the
|
|
64
69
|
// gonext-local-worker daemon's own terminal — swallow it here to keep the REPL terse.
|
|
@@ -142,16 +147,19 @@ if (!workerKey || !apiBase) {
|
|
|
142
147
|
// for piped/scripted input, harmless for interactive TTYs. Both ask() and the main
|
|
143
148
|
// loop consume from this queue (never rl.question, to avoid double-capture).
|
|
144
149
|
//
|
|
145
|
-
// terminal:
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
const
|
|
150
|
+
// terminal: true (on real TTYs) with readline OWNING the prompt via setPrompt/prompt().
|
|
151
|
+
// History of this setting: plain terminal:true with a MANUALLY-written ">> " once caused
|
|
152
|
+
// a stray "[" artifact — readline redrew the input line using its own (empty, never-set)
|
|
153
|
+
// prompt and cursor tracking that our un-announced writes had made stale. The interim
|
|
154
|
+
// fix (terminal:false) traded that for something worse: the OS's canonical line
|
|
155
|
+
// discipline echoes raw CSI bytes, so arrows/Home/End showed literal "^[[D" and mid-line
|
|
156
|
+
// editing didn't work at all (user-reported). Giving readline the prompt makes its
|
|
157
|
+
// redraw math correct — rl.prompt() resets the cursor state after every burst of our
|
|
158
|
+
// output — and raw mode means backspace/arrows/↑-history all behave like a normal shell.
|
|
159
|
+
const IS_TTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
160
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: IS_TTY });
|
|
161
|
+
const PROMPT = cyan(">> ");
|
|
162
|
+
rl.setPrompt(PROMPT);
|
|
155
163
|
// Side effect of terminal:false above: readline no longer intercepts arrow keys for
|
|
156
164
|
// command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
|
|
157
165
|
// bytes straight through as literal input — the OS's own canonical line discipline
|
|
@@ -189,8 +197,13 @@ const nextLine = () =>
|
|
|
189
197
|
? Promise.resolve(null)
|
|
190
198
|
: new Promise((res) => (_lineWaiter = res));
|
|
191
199
|
const ask = async (q) => {
|
|
192
|
-
|
|
200
|
+
// Route the question through readline's own prompt so its redraw logic stays
|
|
201
|
+
// consistent (a raw stdout write here would re-create the stale-cursor artifact
|
|
202
|
+
// that terminal:true mode is designed to avoid). Restored to ">> " afterwards.
|
|
203
|
+
rl.setPrompt(q);
|
|
204
|
+
rl.prompt();
|
|
193
205
|
const a = await nextLine();
|
|
206
|
+
rl.setPrompt(PROMPT);
|
|
194
207
|
return (a ?? "").trim();
|
|
195
208
|
};
|
|
196
209
|
|
|
@@ -510,14 +523,48 @@ async function runAgentTurn(history) {
|
|
|
510
523
|
}
|
|
511
524
|
inEditCard = false; // card ended — fall through to normal handling
|
|
512
525
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
526
|
+
{
|
|
527
|
+
const t = line.trim();
|
|
528
|
+
const openM = !inCode || t.startsWith("<code") ? CODE_OPEN_RE.exec(t) : null;
|
|
529
|
+
if (openM) {
|
|
530
|
+
// Enter code mode (or stay in it on a duplicate tag). Anything glued after
|
|
531
|
+
// the tag on the same line is code — print it, minus a glued close tag.
|
|
532
|
+
stampIfThinking(); // hold/refresh the stamp; the code body commits it below
|
|
533
|
+
inCode = true;
|
|
534
|
+
let rest = t.slice(openM[0].length).trim();
|
|
535
|
+
if (CODE_CLOSE_RE.test(rest)) {
|
|
536
|
+
inCode = false;
|
|
537
|
+
rest = rest.replace(CODE_CLOSE_RE, "").trim();
|
|
538
|
+
}
|
|
539
|
+
if (rest) {
|
|
540
|
+
onContent();
|
|
541
|
+
process.stdout.write(codeColor(rest) + "\n");
|
|
542
|
+
lastWasBlank = false;
|
|
543
|
+
}
|
|
544
|
+
lastContentAt = Date.now();
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!inCode && isBareCloseTag(t)) {
|
|
548
|
+
// Stray close tag with no open in sight — swallow, never show it.
|
|
549
|
+
lastContentAt = Date.now();
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
517
552
|
}
|
|
518
553
|
if (inCode) {
|
|
519
|
-
|
|
520
|
-
if (
|
|
554
|
+
const t = line.trim();
|
|
555
|
+
if (CODE_CLOSE_RE.test(t)) {
|
|
556
|
+
// Close tag, possibly with the code's last line glued in front of it.
|
|
557
|
+
const before = t.replace(CODE_CLOSE_RE, "").trim();
|
|
558
|
+
if (before) {
|
|
559
|
+
onContent();
|
|
560
|
+
process.stdout.write(codeColor(before) + "\n");
|
|
561
|
+
lastWasBlank = false;
|
|
562
|
+
}
|
|
563
|
+
inCode = false;
|
|
564
|
+
lastContentAt = Date.now();
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (t === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
|
|
521
568
|
onContent();
|
|
522
569
|
process.stdout.write(codeColor(line) + "\n");
|
|
523
570
|
lastWasBlank = false;
|
|
@@ -637,20 +684,29 @@ async function main() {
|
|
|
637
684
|
dim(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.\n`)
|
|
638
685
|
);
|
|
639
686
|
}
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
process.
|
|
687
|
+
// Ctrl-C arrives on ONE of two paths depending on the readline mode: with
|
|
688
|
+
// terminal:true (real TTY) readline's raw-mode byte scanning intercepts \x03 and
|
|
689
|
+
// emits rl "SIGINT" (no OS signal is delivered); with terminal:false (piped input)
|
|
690
|
+
// the OS delivers a real SIGINT to the process. Attach the same handler to both —
|
|
691
|
+
// only one can ever fire for a given mode, so there's no double-handling.
|
|
692
|
+
const onInterrupt = () => {
|
|
644
693
|
if (following) {
|
|
645
694
|
followAborted = true;
|
|
646
695
|
process.stdout.write(red("\n^C "));
|
|
647
696
|
} else {
|
|
648
|
-
process.stdout.write(dim("\n(/exit to quit)\n")
|
|
697
|
+
process.stdout.write(dim("\n(/exit to quit)\n"));
|
|
698
|
+
// Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
|
|
699
|
+
// redraw through readline so its cursor tracking stays right.
|
|
700
|
+
rl.line = "";
|
|
701
|
+
rl.cursor = 0;
|
|
702
|
+
rl.prompt();
|
|
649
703
|
}
|
|
650
|
-
}
|
|
704
|
+
};
|
|
705
|
+
process.on("SIGINT", onInterrupt);
|
|
706
|
+
rl.on("SIGINT", onInterrupt);
|
|
651
707
|
|
|
652
708
|
for (;;) {
|
|
653
|
-
|
|
709
|
+
rl.prompt();
|
|
654
710
|
// Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
|
|
655
711
|
// typed while the agent was busy on a long turn (queued, not yet read) all drain at
|
|
656
712
|
// once the moment the turn ends, each re-printing ">> " with no newline between them
|
package/gonext_agent_chat.py
CHANGED
|
@@ -659,6 +659,36 @@ def _is_trivial_chat(text: str) -> bool:
|
|
|
659
659
|
return bool(_TRIVIAL_CHAT.match(t))
|
|
660
660
|
|
|
661
661
|
|
|
662
|
+
# The greeting/smalltalk set MINUS the coding-ambiguous words: in a registered
|
|
663
|
+
# workspace (the `gonext` terminal), a bare "test"/"testing"/"ping" leans toward a
|
|
664
|
+
# command ("run the tests"), so it should reach the agent, not be answered as chat.
|
|
665
|
+
# This is the ONLY escape from workspace-mode's default-to-agent routing, so it must
|
|
666
|
+
# stay tight — anything not obviously conversational is better handled as a task.
|
|
667
|
+
_GREETING_SMALLTALK = re.compile(
|
|
668
|
+
r"^\s*(?:"
|
|
669
|
+
r"h(?:i+|ello+|ey+|iya|owdy)|yo|sup|wass?up|"
|
|
670
|
+
r"good\s*(?:morning|afternoon|evening|night|day)|greetings|"
|
|
671
|
+
r"thanks?(?:\s*you)?|thank\s*you|thx|ty|cheers|much\s*appreciated|"
|
|
672
|
+
r"bye+|goodbye|see\s*(?:ya|you)|cya|later|good\s*night|"
|
|
673
|
+
r"ok(?:ay)?|k|cool|nice|great|awesome|perfect|sounds\s*good|got\s*it|"
|
|
674
|
+
r"how\s*(?:are|r)\s*(?:you|u|ya)(?:\s*doing)?|how'?s\s*it\s*going|what'?s\s*up|"
|
|
675
|
+
r"who\s*(?:are|r)\s*(?:you|u)|what\s*(?:can|do)\s*you\s*do|what\s*are\s*you"
|
|
676
|
+
r")"
|
|
677
|
+
r"(?:\s+(?:there|everyone|all|bot|assistant|gonext|man|dude|buddy))?"
|
|
678
|
+
r"[\s!.?,'\"]*$",
|
|
679
|
+
re.IGNORECASE,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _is_greeting_smalltalk(text: str) -> bool:
|
|
684
|
+
"""Tighter than _is_trivial_chat (no test/ping) — the single chat escape from
|
|
685
|
+
workspace-mode default-to-agent routing."""
|
|
686
|
+
t = (text or "").strip()
|
|
687
|
+
if not t or len(t) > 60:
|
|
688
|
+
return False
|
|
689
|
+
return bool(_GREETING_SMALLTALK.match(t))
|
|
690
|
+
|
|
691
|
+
|
|
662
692
|
# A weak/confused coding model sometimes "thinks out loud" in plain prose instead of
|
|
663
693
|
# emitting the next tool call — e.g. a rambling, self-correcting scratchpad ("Wait,
|
|
664
694
|
# actually... Let's try... Actually, let's just...") that can run thousands of chars.
|
|
@@ -2514,14 +2544,31 @@ def run_agent_chat(cfg):
|
|
|
2514
2544
|
and not _HOWTO_PREFIX_RE.match(t)
|
|
2515
2545
|
and _WS_ACTION_RE.search(t))
|
|
2516
2546
|
|
|
2517
|
-
#
|
|
2518
|
-
#
|
|
2519
|
-
#
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2547
|
+
# Routing. Two fundamentally different contexts:
|
|
2548
|
+
#
|
|
2549
|
+
# (A) WORKSPACE MODE (a folder is registered — i.e. the `gonext` terminal, or web
|
|
2550
|
+
# with a workspace): DEFAULT TO THE AGENT. The user opened a code folder and
|
|
2551
|
+
# registered it for the agent to read/edit/run — everything they type is
|
|
2552
|
+
# presumptively a task to DO, not a question to classify. We do NOT ask a model
|
|
2553
|
+
# classifier "does this need tools?" here: on a weak local model that guess is
|
|
2554
|
+
# unreliable, and every wrong guess sends an action request ("create a reactjs
|
|
2555
|
+
# project and start it", "do it for me") to plain chat, which can only hand back
|
|
2556
|
+
# instructions — the exact instability the user rejected. The ONLY escape is the
|
|
2557
|
+
# deterministic greeting/thanks fast-path, so "hi"/"thanks" stay instant instead
|
|
2558
|
+
# of spinning up the coding model. Anything else → agent, which still just
|
|
2559
|
+
# answers via final_answer when no tool is needed.
|
|
2560
|
+
#
|
|
2561
|
+
# (B) NO WORKSPACE (plain web chat): keep the keyword + model classifier — there are
|
|
2562
|
+
# no file/run tools to act with, so chat-vs-tool routing still earns its keep.
|
|
2563
|
+
_emit({"type": "step", "text": "Routing your request…"})
|
|
2564
|
+
if _is_greeting_smalltalk(latest_user_text) and not _AGENT_KEYWORDS.search(latest_user_text or ""):
|
|
2565
|
+
_log("router → NO (greeting/smalltalk — local fast-path)")
|
|
2523
2566
|
_emit({"type": "step", "text": "→ Chat reply"})
|
|
2524
2567
|
needs_agent = False
|
|
2568
|
+
elif _WS_AVAILABLE:
|
|
2569
|
+
_log("router → YES (workspace registered → act by default, no classifier)")
|
|
2570
|
+
_emit({"type": "step", "text": "→ Agent mode (workspace)"})
|
|
2571
|
+
needs_agent = True
|
|
2525
2572
|
else:
|
|
2526
2573
|
needs_agent = _route(latest_user_text, agent_base_url, agent_api_key, agent_model_id)
|
|
2527
2574
|
# A pending email awaiting 'confirm' must reach the agent even though a bare
|
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.200",
|
|
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",
|