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

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.
@@ -1506,7 +1506,7 @@ async function runAgentChatJob(job) {
1506
1506
  codingBaseURL: payload?.codingBaseURL ?? "",
1507
1507
  codingModelId: payload?.codingModelId ?? "",
1508
1508
  tools: payload?.tools ?? ["http_request"],
1509
- maxSteps: payload?.maxSteps ?? 8,
1509
+ maxSteps: payload?.maxSteps ?? 5,
1510
1510
  // Tool-invocation mode for the agent: "code" (default, python CodeAgent) or
1511
1511
  // "toolcall" (structured JSON tool calls). Passed through only; the API/web can
1512
1512
  // set payload.agentToolMode to A/B — default keeps current behavior.
@@ -11,7 +11,7 @@ Reads on stdin:
11
11
  "codingBaseURL": str, # optional: dedicated coding/reasoning model for the
12
12
  "codingModelId": str, # CodeAgent's tool-use loop; empty = reuse agentModelId
13
13
  "tools": ["http_request"], # v1: only http_request
14
- "maxSteps": int # multi-step ReAct budget; default 8
14
+ "maxSteps": int # multi-step ReAct budget; default 5
15
15
  }
16
16
 
17
17
  Emits NDJSON lines on stdout:
@@ -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
@@ -65,13 +66,18 @@ def _http_request_impl(method, url, headers=None, body=None, timeout=25):
65
66
  return f"Error: {e}"
66
67
 
67
68
 
68
- def _html_to_text(html_text, limit=4000):
69
+ def _html_to_text(html_text, limit=3000):
69
70
  """Strip HTML to readable plain text (zero-dep). Used by fetch_url so the weak
70
71
  model receives prose instead of raw tags it cannot parse."""
71
72
  import html as _html
72
73
  text = html_text or ""
73
74
  # Drop script/style/head/svg noise wholesale (incl. their content).
74
75
  text = re.sub(r"(?is)<(script|style|head|noscript|svg|template)\b.*?</\1>", " ", text)
76
+ # Drop page chrome wholesale too — nav menus, headers, footers, sidebars, forms.
77
+ # Without this, a Wikipedia fetch spends ~1KB of the budget on "Jump to content /
78
+ # Main menu / Donate / Create account…" before any article text, which both starves
79
+ # the model of real content and bloats the per-step context (OOM risk on local MLX).
80
+ text = re.sub(r"(?is)<(nav|header|footer|aside|form|button|menu)\b.*?</\1>", " ", text)
75
81
  # Turn block-ending tags into newlines so document structure survives stripping.
76
82
  text = re.sub(r"(?i)<(br|/p|/div|/li|/tr|/h[1-6]|/section|/article)\s*>", "\n", text)
77
83
  # Remove all remaining tags.
@@ -968,7 +974,7 @@ def run_agent_chat(cfg):
968
974
  # overridable via cfg.maxSteps. The provide_final_answer override below is now only
969
975
  # the exhaustion fallback (loop ends without final_answer).
970
976
  try:
971
- max_steps = int(cfg.get("maxSteps") or 8)
977
+ max_steps = int(cfg.get("maxSteps") or 5)
972
978
  except (TypeError, ValueError):
973
979
  max_steps = 8
974
980
  if max_steps < 1:
@@ -1270,6 +1276,10 @@ def run_agent_chat(cfg):
1270
1276
  "- If a response starts with 'HTTP 2' it SUCCEEDED — answer with it next step.\n"
1271
1277
  "- NEVER pass an 'Error:' / HTTP 4xx / 5xx result to final_answer. On an error, try a "
1272
1278
  "DIFFERENT approach next step (e.g. web_search); only give up after a few tries.\n"
1279
+ "- CONVERGE — do NOT keep searching for a 'more complete' or 'perfect' source; a single "
1280
+ "clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
1281
+ "finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
1282
+ "otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
1273
1283
  "- Do NOT put final_answer outside the code block.\n\n"
1274
1284
  )
1275
1285
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1289,6 +1299,26 @@ def run_agent_chat(cfg):
1289
1299
  # tool returned (no extra model call) if the loop ends without final_answer.
1290
1300
  _last_obs: dict = {"text": ""}
1291
1301
 
1302
+ # Deterministic loop-breaker. Weak models often repeat the SAME search/fetch over and
1303
+ # over (hoping for a "more complete" result) and never converge, burning the whole step
1304
+ # budget. We remember each (tool, arg) signature; on a repeat we DON'T re-run it —
1305
+ # instead we return the prior result plus a hard nudge to move on (compile / create_pdf
1306
+ # / final_answer). Keyed to retrieval tools where the loop actually happens.
1307
+ _seen_calls: dict = {}
1308
+
1309
+ def _dedup(tool_name: str, arg: str, next_hint: str):
1310
+ """Return (is_repeat, message). On a repeat, message steers the model forward."""
1311
+ sig = f"{tool_name}::{(arg or '').strip().lower()}"
1312
+ prior = _seen_calls.get(sig)
1313
+ if prior is not None:
1314
+ _log(f"{tool_name} REPEAT suppressed ({arg[:60]!r}) → nudging model forward")
1315
+ return True, (
1316
+ f"You already ran {tool_name}({arg!r}) — its result is unchanged and shown "
1317
+ f"above. Do NOT call it again. {next_hint}"
1318
+ )
1319
+ _seen_calls[sig] = True
1320
+ return False, ""
1321
+
1292
1322
  @tool
1293
1323
  def http_request(method: str, url: str, headers: str = "", body: str = "",
1294
1324
  username: str = "", password: str = "") -> str:
@@ -1381,6 +1411,13 @@ def run_agent_chat(cfg):
1381
1411
  Args:
1382
1412
  query: What to look up, e.g. 'capital of France' or 'productivity day-to-day method'.
1383
1413
  """
1414
+ dup, nudge = _dedup("web_search", query,
1415
+ "Use the facts you have ALREADY gathered: if the task asks for a "
1416
+ "PDF, call create_pdf(text=..., title=...) now; otherwise call "
1417
+ "final_answer with what you found. Do not keep searching for a "
1418
+ "'complete' source — one may not exist.")
1419
+ if dup:
1420
+ return nudge
1384
1421
  _emit({"type": "step", "text": f"Searching the web → {query[:80]}"})
1385
1422
  result = _web_search_impl(query)
1386
1423
  _log(f"web_search {query[:60]!r} → {result[:80]}")
@@ -1398,6 +1435,12 @@ def run_agent_chat(cfg):
1398
1435
  Args:
1399
1436
  url: The full URL of the page to read, e.g. 'https://example.com/article'.
1400
1437
  """
1438
+ dup, nudge = _dedup("fetch_url", url,
1439
+ "You already have this page's text above. Use it: call "
1440
+ "create_pdf(text=..., title=...) if a PDF was requested, else "
1441
+ "final_answer with what you found.")
1442
+ if dup:
1443
+ return nudge
1401
1444
  _emit({"type": "step", "text": f"Reading page → {url[:80]}"})
1402
1445
  status, ctype, raw = _fetch_page_impl(url)
1403
1446
  if status is None:
@@ -1627,7 +1670,21 @@ def run_agent_chat(cfg):
1627
1670
  # 2. strip any leftover Qwen3 <think>…</think> trace (belt-and-suspenders;
1628
1671
  # thinking is disabled via /no_think in _prepare_completion_kwargs, but a
1629
1672
  # stray trace must never reach the parser or leak to the user).
1630
- msg = super().generate(*args, **kwargs)
1673
+ # A transient "Connection error" to the local MLX server (seen mid-loop when the
1674
+ # context grows large) otherwise aborts the whole run — retry a couple of times
1675
+ # with a short backoff before giving up.
1676
+ last_err = None
1677
+ for attempt in range(3):
1678
+ try:
1679
+ msg = super().generate(*args, **kwargs)
1680
+ break
1681
+ except Exception as e: # noqa: BLE001
1682
+ last_err = e
1683
+ if attempt < 2:
1684
+ _log(f"generate transient error (attempt {attempt + 1}/3): {e} — retrying")
1685
+ time.sleep(1.5 * (attempt + 1))
1686
+ else:
1687
+ raise last_err
1631
1688
  try:
1632
1689
  content = getattr(msg, "content", None)
1633
1690
  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.129",
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",