@tiens.nguyen/gonext-local-worker 1.0.312 → 1.0.314
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 +12 -1
- package/gonext_agent_chat.py +55 -0
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -948,6 +948,11 @@ let cancelRequested = false; // a cancel POST has been sent for the current turn
|
|
|
948
948
|
let approvalActive = false;
|
|
949
949
|
let approvalSel = 0; // 0 = Yes (default highlighted), 1 = No
|
|
950
950
|
let approvalResolve = null;
|
|
951
|
+
// The active turn's live-status wiper (its local clearStatus), or null between turns. A
|
|
952
|
+
// picker calls this before drawing so the "Almost done…/thought/token" block is gone
|
|
953
|
+
// first — otherwise the 120ms ticker keeps redrawing that block on top of the picker and
|
|
954
|
+
// its relative-cursor math lands on the wrong rows (duplicated/interleaved options).
|
|
955
|
+
let clearActiveStatus = null;
|
|
951
956
|
function drawApprovalOptions(redraw) {
|
|
952
957
|
// Two option lines, redrawn in place. On redraw the cursor sits one row below "No";
|
|
953
958
|
// step up 2 rows, clear+rewrite both, landing back where we started.
|
|
@@ -986,6 +991,10 @@ const MAXSTEP_PREFIX = "__MAXSTEP__::";
|
|
|
986
991
|
const TIMEOUT_PREFIX = "__TIMEOUT__::";
|
|
987
992
|
const COMPACT_PREFIX = "__COMPACT__::";
|
|
988
993
|
async function approvalPrompt(command) {
|
|
994
|
+
// Wipe the live "thinking" status block BEFORE any picker draws, and stop the ticker from
|
|
995
|
+
// redrawing it (the tick early-returns while a picker is active) — otherwise the block
|
|
996
|
+
// reappears under the options and corrupts the layout.
|
|
997
|
+
if (clearActiveStatus) clearActiveStatus();
|
|
989
998
|
// Context-compaction prompt (task #87 rec#3): the running context got large. Ask whether
|
|
990
999
|
// to aggressively compact older steps. No answer in 30s = No (keep full detail — the safe
|
|
991
1000
|
// default; the always-on auto-trim still bounds things).
|
|
@@ -1463,10 +1472,11 @@ async function runAgentTurn(history) {
|
|
|
1463
1472
|
process.stdout.write(seq);
|
|
1464
1473
|
statusShown = false;
|
|
1465
1474
|
};
|
|
1475
|
+
clearActiveStatus = clearStatus; // let pickers wipe this turn's status block before they draw
|
|
1466
1476
|
|
|
1467
1477
|
const tick = () => {
|
|
1468
1478
|
if (!following || followAborted || !jobRunning) return;
|
|
1469
|
-
if (approvalActive) return; // a
|
|
1479
|
+
if (approvalActive || listPickerActive) return; // a picker owns the screen — don't draw over it
|
|
1470
1480
|
if (Date.now() - lastContentAt < QUIET_MS) return; // tokens still flowing
|
|
1471
1481
|
// A plain-reply answer streams onto a line with NO trailing newline until it's fully
|
|
1472
1482
|
// done — overwriting the ticker there corrupts the visible answer mid-word. Stay
|
|
@@ -2009,6 +2019,7 @@ async function runAgentTurn(history) {
|
|
|
2009
2019
|
} finally {
|
|
2010
2020
|
clearInterval(ticker);
|
|
2011
2021
|
clearStatus();
|
|
2022
|
+
clearActiveStatus = null; // no turn owns the status wiper between turns
|
|
2012
2023
|
// Task #96: stop mouse-reporting the moment the turn ends (also on cancel/error — this
|
|
2013
2024
|
// finally always runs), and detach the click parser, so the user's shell selection is
|
|
2014
2025
|
// never captured between turns.
|
package/gonext_agent_chat.py
CHANGED
|
@@ -4960,6 +4960,7 @@ def run_agent_chat(cfg):
|
|
|
4960
4960
|
_log("streamed generate: request issued (stream=True), awaiting first token…")
|
|
4961
4961
|
stream = self.client.chat.completions.create(**completion_kwargs)
|
|
4962
4962
|
parts = []
|
|
4963
|
+
rparts = [] # reasoning-channel text (task #111: may hold the <code> block)
|
|
4963
4964
|
role = "assistant"
|
|
4964
4965
|
in_tok = out_tok = reasoning_len = 0
|
|
4965
4966
|
first_token_at = None
|
|
@@ -5028,6 +5029,7 @@ def run_agent_chat(cfg):
|
|
|
5028
5029
|
if rpiece:
|
|
5029
5030
|
_mark_first()
|
|
5030
5031
|
reasoning_len += len(rpiece)
|
|
5032
|
+
rparts.append(rpiece)
|
|
5031
5033
|
_stream_buf.append(rpiece)
|
|
5032
5034
|
_stream_chars += len(rpiece)
|
|
5033
5035
|
_emit({"type": "stream", "text": rpiece})
|
|
@@ -5055,10 +5057,63 @@ def run_agent_chat(cfg):
|
|
|
5055
5057
|
_emit({"type": "step", "text": "Model output ran away — restarting this step…"})
|
|
5056
5058
|
_log(f"streamed generate: discarded {len(content)} runaway chars → empty content")
|
|
5057
5059
|
return "", role, in_tok, out_tok, reasoning_len
|
|
5060
|
+
# Task #111: reasoning models on an OpenAI-compatible API (e.g. Kimi K3 @ Moonshot)
|
|
5061
|
+
# can emit the actionable <code>…</code> into the REASONING channel, leaving only
|
|
5062
|
+
# prose + a stray tag in `content` → smolagents' parser can't find the pair. When
|
|
5063
|
+
# content has no usable code block but the reasoning stream does, rebuild content
|
|
5064
|
+
# from it. Inert when reasoning is empty (Ollama with thinking off) → path untouched.
|
|
5065
|
+
content = self._recover_code_from_channels(content, "".join(rparts))
|
|
5058
5066
|
_log(f"streamed generate assembled {len(content)} chars "
|
|
5059
5067
|
f"(in={in_tok} out={out_tok} tokens, reasoning={reasoning_len} chars)")
|
|
5060
5068
|
return content, role, in_tok, out_tok, reasoning_len
|
|
5061
5069
|
|
|
5070
|
+
def _recover_code_from_channels(self, content, reasoning):
|
|
5071
|
+
"""Recover a <code>…</code> tool call that a reasoning model placed in the
|
|
5072
|
+
REASONING channel instead of the visible message (task #111). No-op when the
|
|
5073
|
+
visible content already has a usable code block, or when there is no reasoning
|
|
5074
|
+
text (e.g. Ollama with reasoning_effort='none') — so the Ollama path is unchanged.
|
|
5075
|
+
|
|
5076
|
+
Kimi K3 @ Moonshot splits its output two ways when it fails: (a) the full
|
|
5077
|
+
<code>…</code> block lands in reasoning and only prose leaks to content; or
|
|
5078
|
+
(b) the OPENER + code land in reasoning while the CLOSING </code> leaks into
|
|
5079
|
+
content (seen live: content = 'Thought: …contains.</code>'). Handle both."""
|
|
5080
|
+
_pair = r"<code[^>]*>[\s\S]*?</code>"
|
|
5081
|
+
if re.search(_pair, content or "", re.I):
|
|
5082
|
+
return content # content already has a usable block — never override it
|
|
5083
|
+
reasoning = reasoning or ""
|
|
5084
|
+
if not reasoning.strip():
|
|
5085
|
+
return content # nothing to recover from (Ollama / non-reasoning model)
|
|
5086
|
+
code = None
|
|
5087
|
+
# (a) Prefer a COMPLETE block already inside reasoning (the model drafts the
|
|
5088
|
+
# whole tool call there) — take the LAST one, ignoring any stray </code> in content.
|
|
5089
|
+
last = None
|
|
5090
|
+
for last in re.finditer(r"<code[^>]*>([\s\S]*?)</code>", reasoning, re.I):
|
|
5091
|
+
pass
|
|
5092
|
+
if last:
|
|
5093
|
+
code = last.group(1)
|
|
5094
|
+
else:
|
|
5095
|
+
# (b) Opener + code in reasoning, closer leaked to content → take everything
|
|
5096
|
+
# after the LAST opener in reasoning (its </code> is the one now in content).
|
|
5097
|
+
m = None
|
|
5098
|
+
for m in re.finditer(r"<code[^>]*>([\s\S]*)$", reasoning, re.I):
|
|
5099
|
+
pass
|
|
5100
|
+
if m:
|
|
5101
|
+
code = m.group(1)
|
|
5102
|
+
if code is None:
|
|
5103
|
+
return content
|
|
5104
|
+
# A leaked 'Thought:' line (or a stray </code>) can ride along after the code —
|
|
5105
|
+
# cut it so only the executable snippet remains.
|
|
5106
|
+
code = re.split(r"\n\s*Thought\s*:\s", code, maxsplit=1)[0]
|
|
5107
|
+
code = re.sub(r"</?code[^>]*>", "", code, flags=re.I).strip()
|
|
5108
|
+
if not code:
|
|
5109
|
+
return content
|
|
5110
|
+
# Keep the visible Thought prose (strip any stray/unbalanced code tags), then
|
|
5111
|
+
# append the recovered block so smolagents parses a clean Thought → <code> pair.
|
|
5112
|
+
prose = re.sub(r"</?code[^>]*>", "", content or "", flags=re.I).strip()
|
|
5113
|
+
rebuilt = (prose + "\n" if prose else "") + "<code>\n" + code + "\n</code>"
|
|
5114
|
+
_log(f"#111 recovered <code> block from reasoning channel ({len(code)} chars)")
|
|
5115
|
+
return rebuilt
|
|
5116
|
+
|
|
5062
5117
|
# Injected when a turn produced ONLY reasoning and no message content — a hard,
|
|
5063
5118
|
# model-agnostic steer to emit the actionable code block instead of more analysis.
|
|
5064
5119
|
_CODE_NOW_DIRECTIVE = (
|
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.314",
|
|
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",
|