@tiens.nguyen/gonext-local-worker 1.0.138 → 1.0.141

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.
@@ -1526,14 +1526,15 @@ async function runAgentChatJob(job) {
1526
1526
  emailFrom: payload?.emailFrom ?? "",
1527
1527
  emailAllowList: payload?.emailAllowList ?? "",
1528
1528
  });
1529
- // 10 min max for an agent run: 5 ReAct steps on a 14B with a cold prompt cache —
1530
- // or a remote Ollama box cold-loading a big model — can exceed the old caps. Must
1531
- // stay BELOW the web client's hard WS deadline (660s) so our clean timeout error
1532
- // reaches the user instead of a generic WS timeout. On expiry the python child is
1533
- // SIGKILLed, which also shows up as a BrokenPipeError in the model server log
1534
- // (expected). Liveness during long model calls comes from the python heartbeat
1535
- // (a keepalive step chunk every 45s), not from this cap.
1536
- const timeoutMs = 600_000;
1529
+ // 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
1530
+ // cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
1531
+ // for many minutes per step. Must stay BELOW the web client's hard WS deadline
1532
+ // (1_860_000 ms) so our clean timeout error reaches the user instead of a generic
1533
+ // WS timeout. On expiry the python child is SIGKILLed, which also shows up as a
1534
+ // BrokenPipeError in the model server log (expected). Liveness during long model
1535
+ // calls comes from the python heartbeat (a keepalive step chunk every 45s), not
1536
+ // from this cap.
1537
+ const timeoutMs = 1_800_000;
1537
1538
 
1538
1539
  let inThink = false;
1539
1540
  let finalText = "";
@@ -398,6 +398,24 @@ def _list_model_ids(base_url, api_key=""):
398
398
  if isinstance(d, dict) and isinstance(d.get("id"), str) and d["id"].strip()]
399
399
 
400
400
 
401
+ def _is_ollama_server(base_url):
402
+ """True if the OpenAI-compatible base URL is an Ollama server.
403
+
404
+ Ollama's root path answers GET / with the plain-text banner "Ollama is
405
+ running". Used to gate Ollama-only request params (reasoning_effort) that an
406
+ MLX server might reject.
407
+ """
408
+ root = re.sub(r"/v1/?$", "", (base_url or "").rstrip("/"))
409
+ if not root:
410
+ return False
411
+ req = urllib.request.Request(root, headers={"User-Agent": "gonext-agent/1.0"})
412
+ try:
413
+ with urllib.request.urlopen(req, timeout=6, context=_ssl_context()) as resp:
414
+ return b"ollama" in resp.read(200).lower()
415
+ except Exception: # noqa: BLE001
416
+ return False
417
+
418
+
401
419
  def _detect_model_id(base_url, api_key=""):
402
420
  """Ask an OpenAI-compatible server which model it serves (first reported id).
403
421
 
@@ -616,6 +634,38 @@ def _strip_tool_tags(text: str) -> str:
616
634
 
617
635
  _THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
618
636
 
637
+ # Whitespace/attr variants of smolagents' code-block tags. Weak models routinely
638
+ # emit "<code >" (trailing space), "< code >", "</ code>", or an uppercase form —
639
+ # none of which match smolagents' exact "<code>(.*?)</code>" parser, so EVERY step
640
+ # fails with "regex pattern <code>(.*?)</code> was not found" and the run burns all
641
+ # its steps. Normalizing the tags to the canonical form before the parser sees them
642
+ # turns that spiral into a normal, parseable step.
643
+ _CODE_OPEN_TAG = re.compile(r"<\s*code(?:\s[^>]*)?\s*>", re.IGNORECASE)
644
+ _CODE_CLOSE_TAG = re.compile(r"<\s*/\s*code\s*>", re.IGNORECASE)
645
+ # Fallback: a markdown code fence (```py / ```python / ```) when the model ignored
646
+ # the tag convention entirely. Captures the fenced body so we can re-wrap it.
647
+ _MD_FENCE_BLOCK = re.compile(
648
+ r"```(?:py|python)?[ \t]*\r?\n(.*?)```", re.DOTALL | re.IGNORECASE
649
+ )
650
+
651
+
652
+ def _normalize_code_tags(text: str) -> str:
653
+ """Coerce a model reply's code-block delimiters to smolagents' exact <code>…</code>.
654
+
655
+ Handles the two failure modes seen live with weak coding models:
656
+ 1. tag whitespace/case: "<code >", "< code >", "</ code>", "<CODE>" → "<code>".
657
+ 2. markdown fences instead of tags: ```py … ``` → <code> … </code> (only when
658
+ no <code> tag is present after step 1, so we don't touch code that legitimately
659
+ contains backticks inside a real <code> block).
660
+ Returns text unchanged when it already parses cleanly."""
661
+ if not text:
662
+ return text
663
+ fixed = _CODE_OPEN_TAG.sub("<code>", text)
664
+ fixed = _CODE_CLOSE_TAG.sub("</code>", fixed)
665
+ if "<code>" not in fixed:
666
+ fixed = _MD_FENCE_BLOCK.sub(lambda m: f"<code>\n{m.group(1)}</code>", fixed, count=1)
667
+ return fixed
668
+
619
669
 
620
670
  def _strip_think(text: str) -> str:
621
671
  """Remove Qwen3-style <think>…</think> reasoning traces from a model reply.
@@ -1823,10 +1873,20 @@ def run_agent_chat(cfg):
1823
1873
  if content is None:
1824
1874
  _log("model returned content=None (empty/thinking-only turn) → coercing to ''")
1825
1875
  msg.content = ""
1876
+ content = ""
1826
1877
  elif isinstance(content, str) and "</think>" in content.lower():
1827
1878
  cleaned = _strip_think(content)
1828
1879
  _log(f"stripped <think> trace ({len(content) - len(cleaned)} chars)")
1829
1880
  msg.content = cleaned
1881
+ content = cleaned
1882
+ # Normalize code-block delimiters so smolagents' <code>…</code> parser
1883
+ # matches even when the model wrote "<code >" / a markdown fence. Without
1884
+ # this, tag-whitespace variants fail EVERY step → "Reached max steps".
1885
+ if isinstance(content, str) and content:
1886
+ normalized = _normalize_code_tags(content)
1887
+ if normalized != content:
1888
+ _log("normalized code-block tags (<code >/fence → <code>)")
1889
+ msg.content = normalized
1830
1890
  except Exception as e: # noqa: BLE001
1831
1891
  _log(f"content-normalize error: {e}")
1832
1892
  return msg
@@ -1948,16 +2008,27 @@ def run_agent_chat(cfg):
1948
2008
  return ChatMessage(role=MessageRole.ASSISTANT, content=text)
1949
2009
 
1950
2010
  try:
2011
+ # Ollama's OpenAI-compat endpoint ignores Qwen3's /no_think soft-switch (its
2012
+ # template forces thinking mode; "think": false is also ignored there) but DOES
2013
+ # honor reasoning_effort="none". Thinking burned ~430 of 487 output tokens in
2014
+ # the live test — minutes per step on a slow GPU — so disable it explicitly.
2015
+ # Sniffed per-server because MLX might reject the unknown param.
2016
+ extra_model_kwargs = {}
2017
+ if _is_ollama_server(coding_base_url):
2018
+ _log("coding server is Ollama → sending reasoning_effort='none' to disable thinking")
2019
+ extra_model_kwargs["reasoning_effort"] = "none"
1951
2020
  model = _LoggingModel(
1952
2021
  model_id=coding_model_id,
1953
2022
  api_base=coding_base_url,
1954
2023
  api_key=agent_api_key,
1955
- # Cap each HTTP attempt at 240s so one hung request (remote server
1956
- # cold-loading a big model) fails into OUR retry loop instead of eating
1957
- # the whole worker budget; by the retry the model is usually loaded.
2024
+ # Cap each HTTP attempt at 600s so a genuinely slow single call (a
2025
+ # remote M6000/Vulkan box doing a cold ~4-5min prompt-eval) can finish
2026
+ # on the first try, while a truly hung request still fails into OUR
2027
+ # retry loop rather than eating the whole 30min worker budget.
1958
2028
  # max_retries=0: the openai client's own silent retries would otherwise
1959
2029
  # stack multiplicatively with ours (3 × 3 attempts).
1960
- client_kwargs={"timeout": 240.0, "max_retries": 0},
2030
+ client_kwargs={"timeout": 600.0, "max_retries": 0},
2031
+ **extra_model_kwargs,
1961
2032
  )
1962
2033
  agent_tools = [http_request, web_search, fetch_url, calculate,
1963
2034
  get_current_datetime, create_pdf]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.138",
3
+ "version": "1.0.141",
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",