@tiens.nguyen/gonext-local-worker 1.0.189 → 1.0.191
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 +7 -0
- package/gonext_agent_chat.py +40 -14
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -306,6 +306,13 @@ async function runAgentTurn(history) {
|
|
|
306
306
|
const tick = () => {
|
|
307
307
|
if (!following || followAborted || !jobRunning) return;
|
|
308
308
|
if (Date.now() - lastContentAt < QUIET_MS) return; // tokens still flowing
|
|
309
|
+
// A plain-reply answer streams onto a line with NO trailing newline until it's fully
|
|
310
|
+
// done — if the model pauses mid-sentence (normal token-generation jitter) and we're
|
|
311
|
+
// already partway through that open line, CLEAR_LINE can only erase the terminal's
|
|
312
|
+
// CURRENT visual row, not the whole logical line once it's wrapped — overwriting the
|
|
313
|
+
// ticker there corrupts the visible answer text mid-word. Stay silent once any of
|
|
314
|
+
// THIS line has already been shown; the next completed line (fresh, empty) is safe.
|
|
315
|
+
if (plainReplyFlow && carryFlushedLen > 0) return;
|
|
309
316
|
// Count from when the stream actually went quiet, so the seconds reflect the true
|
|
310
317
|
// wait for the first token (1s, 2s, 3s…) rather than from the first tick.
|
|
311
318
|
if (!thinking) { thinking = true; thinkingSince = lastContentAt; thinkWord = pickWord(); }
|
package/gonext_agent_chat.py
CHANGED
|
@@ -829,7 +829,13 @@ def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str,
|
|
|
829
829
|
and delivering it in one burst) — the worker renders these as the live-updating MAIN
|
|
830
830
|
answer, not the collapsible Thinking panel. If the primary model is unreachable (e.g.
|
|
831
831
|
the local MLX server is down) and a distinct fallback model is given (the coding
|
|
832
|
-
model), retry there so a greeting still gets answered instead of erroring.
|
|
832
|
+
model), retry there so a greeting still gets answered instead of erroring.
|
|
833
|
+
|
|
834
|
+
RAISES (does not return a sentinel string) if every target fails or returns no
|
|
835
|
+
content — callers that want a soft degrade should catch this locally and substitute
|
|
836
|
+
their own clean message; letting it propagate uncaught turns into a genuine job
|
|
837
|
+
failure (red error, never persisted to conversation history) instead of a fabricated
|
|
838
|
+
"answer" that would poison every future turn's prompt with raw exception text."""
|
|
833
839
|
_THINK_RE_LOCAL = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
|
|
834
840
|
chat_messages = [{"role": "system", "content": "You are a helpful assistant."}]
|
|
835
841
|
for m in messages:
|
|
@@ -863,24 +869,38 @@ def _plain_reply(messages: list, base_url: str, api_key: str, model_id: str,
|
|
|
863
869
|
from openai import OpenAI
|
|
864
870
|
client = OpenAI(base_url=b, api_key=api_key or "local",
|
|
865
871
|
max_retries=0, timeout=60)
|
|
866
|
-
|
|
872
|
+
text = _chat_create_stream(
|
|
867
873
|
client, _track,
|
|
868
874
|
model=mid,
|
|
869
875
|
messages=chat_messages,
|
|
870
876
|
temperature=0.7,
|
|
871
877
|
max_tokens=512,
|
|
872
878
|
).strip()
|
|
879
|
+
if text:
|
|
880
|
+
return text
|
|
881
|
+
# The call SUCCEEDED (no exception) but the model produced NO content at all —
|
|
882
|
+
# seen with a reasoning model given a confusing/degenerate prompt (e.g. a prior
|
|
883
|
+
# turn's history polluted with raw error text). Treat this the same as a
|
|
884
|
+
# failure: fall back to the next target, or surface a clean message, rather
|
|
885
|
+
# than silently returning "" (which the caller can't tell apart from a real,
|
|
886
|
+
# deliberately empty answer).
|
|
887
|
+
last_err = RuntimeError(f"{mid} returned an empty response")
|
|
873
888
|
except Exception as e: # noqa: BLE001
|
|
874
889
|
last_err = e
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
return
|
|
890
|
+
if emitted_here:
|
|
891
|
+
# Part of THIS attempt's answer already streamed to the user — don't
|
|
892
|
+
# silently splice a DIFFERENT model's output after a partial one.
|
|
893
|
+
break
|
|
894
|
+
if i + 1 < len(targets):
|
|
895
|
+
nb, nmid = targets[i + 1] # the model we're about to try next
|
|
896
|
+
_log(f"plain reply: {mid} @ {b} failed ({last_err}); "
|
|
897
|
+
f"falling back to {nmid} @ {nb}")
|
|
898
|
+
# Every target failed or returned nothing — raise rather than return a sentinel
|
|
899
|
+
# string. A returned string always looks like a legitimate answer to callers (and
|
|
900
|
+
# gets persisted to conversation history); raising lets it become a genuine job
|
|
901
|
+
# failure instead (see docstring). Kept short/clean (no embedded newlines/HTML — a
|
|
902
|
+
# raw nginx 502 page can be a whole document) for whichever caller logs/surfaces it.
|
|
903
|
+
raise RuntimeError(f"couldn't get a response ({_clip(str(last_err), 160)})")
|
|
884
904
|
|
|
885
905
|
|
|
886
906
|
def _synthesize_document(gathered: list, task: str, base_url: str, api_key: str,
|
|
@@ -3754,14 +3774,20 @@ def run_agent_chat(cfg):
|
|
|
3754
3774
|
_emit({"type": "final", "text": f"⚠️ {cfg_err}"})
|
|
3755
3775
|
return
|
|
3756
3776
|
# An unexpected failure inside the agent loop should still return a useful
|
|
3757
|
-
# answer from the conversation rather than surfacing a raw error to the user
|
|
3777
|
+
# answer from the conversation rather than surfacing a raw error to the user —
|
|
3778
|
+
# but only when the fallback ACTUALLY has something useful to say.
|
|
3758
3779
|
_log(f"agent error: {e} — degrading to plain reply")
|
|
3759
3780
|
try:
|
|
3760
3781
|
fallback = _plain_reply(messages, agent_base_url, agent_api_key, agent_model_id).strip()
|
|
3761
3782
|
except Exception as e2: # noqa: BLE001
|
|
3762
3783
|
_log(f"plain-reply degrade error: {e2}")
|
|
3763
|
-
|
|
3764
|
-
|
|
3784
|
+
# Nothing useful to say — re-raise the ORIGINAL (more relevant) agent
|
|
3785
|
+
# failure so this becomes a genuine job failure: a red error the REPL/web
|
|
3786
|
+
# already handle correctly, NOT persisted to conversation history. Emitting
|
|
3787
|
+
# a fabricated "final" answer here instead would poison every future turn's
|
|
3788
|
+
# prompt with raw exception text (this is exactly what caused task #35).
|
|
3789
|
+
raise e
|
|
3790
|
+
_emit({"type": "final", "text": fallback})
|
|
3765
3791
|
|
|
3766
3792
|
|
|
3767
3793
|
def _log(text: str):
|
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.191",
|
|
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",
|