@tiens.nguyen/gonext-local-worker 1.0.127 → 1.0.128

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.
@@ -23,6 +23,7 @@ import contextlib
23
23
  import json
24
24
  import re
25
25
  import sys
26
+ import time
26
27
  import traceback
27
28
  import urllib.request
28
29
  import urllib.error
@@ -1270,6 +1271,10 @@ def run_agent_chat(cfg):
1270
1271
  "- If a response starts with 'HTTP 2' it SUCCEEDED — answer with it next step.\n"
1271
1272
  "- NEVER pass an 'Error:' / HTTP 4xx / 5xx result to final_answer. On an error, try a "
1272
1273
  "DIFFERENT approach next step (e.g. web_search); only give up after a few tries.\n"
1274
+ "- CONVERGE — do NOT keep searching for a 'more complete' or 'perfect' source; a single "
1275
+ "clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
1276
+ "finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
1277
+ "otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
1273
1278
  "- Do NOT put final_answer outside the code block.\n\n"
1274
1279
  )
1275
1280
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1289,6 +1294,26 @@ def run_agent_chat(cfg):
1289
1294
  # tool returned (no extra model call) if the loop ends without final_answer.
1290
1295
  _last_obs: dict = {"text": ""}
1291
1296
 
1297
+ # Deterministic loop-breaker. Weak models often repeat the SAME search/fetch over and
1298
+ # over (hoping for a "more complete" result) and never converge, burning the whole step
1299
+ # budget. We remember each (tool, arg) signature; on a repeat we DON'T re-run it —
1300
+ # instead we return the prior result plus a hard nudge to move on (compile / create_pdf
1301
+ # / final_answer). Keyed to retrieval tools where the loop actually happens.
1302
+ _seen_calls: dict = {}
1303
+
1304
+ def _dedup(tool_name: str, arg: str, next_hint: str):
1305
+ """Return (is_repeat, message). On a repeat, message steers the model forward."""
1306
+ sig = f"{tool_name}::{(arg or '').strip().lower()}"
1307
+ prior = _seen_calls.get(sig)
1308
+ if prior is not None:
1309
+ _log(f"{tool_name} REPEAT suppressed ({arg[:60]!r}) → nudging model forward")
1310
+ return True, (
1311
+ f"You already ran {tool_name}({arg!r}) — its result is unchanged and shown "
1312
+ f"above. Do NOT call it again. {next_hint}"
1313
+ )
1314
+ _seen_calls[sig] = True
1315
+ return False, ""
1316
+
1292
1317
  @tool
1293
1318
  def http_request(method: str, url: str, headers: str = "", body: str = "",
1294
1319
  username: str = "", password: str = "") -> str:
@@ -1381,6 +1406,13 @@ def run_agent_chat(cfg):
1381
1406
  Args:
1382
1407
  query: What to look up, e.g. 'capital of France' or 'productivity day-to-day method'.
1383
1408
  """
1409
+ dup, nudge = _dedup("web_search", query,
1410
+ "Use the facts you have ALREADY gathered: if the task asks for a "
1411
+ "PDF, call create_pdf(text=..., title=...) now; otherwise call "
1412
+ "final_answer with what you found. Do not keep searching for a "
1413
+ "'complete' source — one may not exist.")
1414
+ if dup:
1415
+ return nudge
1384
1416
  _emit({"type": "step", "text": f"Searching the web → {query[:80]}"})
1385
1417
  result = _web_search_impl(query)
1386
1418
  _log(f"web_search {query[:60]!r} → {result[:80]}")
@@ -1398,6 +1430,12 @@ def run_agent_chat(cfg):
1398
1430
  Args:
1399
1431
  url: The full URL of the page to read, e.g. 'https://example.com/article'.
1400
1432
  """
1433
+ dup, nudge = _dedup("fetch_url", url,
1434
+ "You already have this page's text above. Use it: call "
1435
+ "create_pdf(text=..., title=...) if a PDF was requested, else "
1436
+ "final_answer with what you found.")
1437
+ if dup:
1438
+ return nudge
1401
1439
  _emit({"type": "step", "text": f"Reading page → {url[:80]}"})
1402
1440
  status, ctype, raw = _fetch_page_impl(url)
1403
1441
  if status is None:
@@ -1627,7 +1665,21 @@ def run_agent_chat(cfg):
1627
1665
  # 2. strip any leftover Qwen3 <think>…</think> trace (belt-and-suspenders;
1628
1666
  # thinking is disabled via /no_think in _prepare_completion_kwargs, but a
1629
1667
  # stray trace must never reach the parser or leak to the user).
1630
- msg = super().generate(*args, **kwargs)
1668
+ # A transient "Connection error" to the local MLX server (seen mid-loop when the
1669
+ # context grows large) otherwise aborts the whole run — retry a couple of times
1670
+ # with a short backoff before giving up.
1671
+ last_err = None
1672
+ for attempt in range(3):
1673
+ try:
1674
+ msg = super().generate(*args, **kwargs)
1675
+ break
1676
+ except Exception as e: # noqa: BLE001
1677
+ last_err = e
1678
+ if attempt < 2:
1679
+ _log(f"generate transient error (attempt {attempt + 1}/3): {e} — retrying")
1680
+ time.sleep(1.5 * (attempt + 1))
1681
+ else:
1682
+ raise last_err
1631
1683
  try:
1632
1684
  content = getattr(msg, "content", None)
1633
1685
  if content is None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.127",
3
+ "version": "1.0.128",
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",