@tiens.nguyen/gonext-local-worker 1.0.123 → 1.0.125

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.
@@ -904,20 +904,33 @@ def run_agent_chat(cfg):
904
904
  raw_coding_base = (cfg.get("codingBaseURL") or "").strip()
905
905
  raw_coding_model = (cfg.get("codingModelId") or "").strip()
906
906
  if raw_coding_base:
907
- # A dedicated coding server is configured. If no model name was given,
908
- # ask the server which model it serves (mlx_lm.server otherwise tries to
909
- # download a mismatched name from HF and 404s).
910
- detected = raw_coding_model or _detect_model_id(raw_coding_base, agent_api_key)
911
- if detected:
912
- coding_base_url = raw_coding_base
913
- coding_model_id = detected
914
- else:
907
+ same_server = raw_coding_base.rstrip("/") == (agent_base_url or "").rstrip("/")
908
+ if same_server and not raw_coding_model:
909
+ # The coding URL points at the SAME server as the chat model. Do NOT
910
+ # auto-detect: /v1/models can list several cached models and return a
911
+ # SMALLER one first (e.g. a 3B), silently downgrading the agent's reasoning.
912
+ # Reuse the chat model id directly — it's the model actually loaded here.
915
913
  _log(
916
- f"coding model id unresolved for {raw_coding_base!r}; "
917
- "falling back to chat model"
914
+ f"coding base == chat base ({raw_coding_base}); "
915
+ f"reusing chat model {agent_model_id!r} (skipping auto-detect)"
918
916
  )
919
917
  coding_base_url = agent_base_url
920
918
  coding_model_id = agent_model_id
919
+ else:
920
+ # A DIFFERENT dedicated coding server. If no model name was given, ask the
921
+ # server which model it serves (mlx_lm.server otherwise tries to download a
922
+ # mismatched name from HF and 404s).
923
+ detected = raw_coding_model or _detect_model_id(raw_coding_base, agent_api_key)
924
+ if detected:
925
+ coding_base_url = raw_coding_base
926
+ coding_model_id = detected
927
+ else:
928
+ _log(
929
+ f"coding model id unresolved for {raw_coding_base!r}; "
930
+ "falling back to chat model"
931
+ )
932
+ coding_base_url = agent_base_url
933
+ coding_model_id = agent_model_id
921
934
  else:
922
935
  coding_base_url = agent_base_url
923
936
  coding_model_id = agent_model_id
@@ -1187,15 +1200,13 @@ def run_agent_chat(cfg):
1187
1200
  "\n"
1188
1201
  "http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
1189
1202
  "\n"
1190
- "BASIC AUTH — ALWAYS use username= and password=, NEVER construct headers manually:\n"
1191
- " response = http_request('GET', 'https://api.example.com/data',\n"
1192
- " username='alice@example.com', password='secret123')\n"
1193
- " final_answer(response)\n"
1194
- "The function handles base64 encoding automatically. NEVER write 'Basic ' + anything.\n"
1195
- "\n"
1196
- "BEARER TOKEN use headers:\n"
1197
- " response = http_request('GET', url, headers='{\"Authorization\": \"Bearer TOKEN\"}')\n"
1198
- " final_answer(response)\n"
1203
+ "AUTH — ONLY when the TASK itself provides credentials or a token. If it does "
1204
+ "NOT, call http_request with NO username/password/headers. NEVER invent or copy "
1205
+ "the placeholder values below.\n"
1206
+ " BASIC AUTH — pass username= and password= (auto base64; never build a 'Basic ' header):\n"
1207
+ " http_request('GET', url, username=<username from the task>, password=<password from the task>)\n"
1208
+ " BEARER TOKEN — pass it in headers:\n"
1209
+ " http_request('GET', url, headers='{\"Authorization\": \"Bearer <token from the task>\"}')\n"
1199
1210
  "\n"
1200
1211
  "CHOOSING A TOOL (match the TASK, not these examples):\n"
1201
1212
  "- ONLY a date/time question (e.g. 'what is the date today') -> get_current_datetime().\n"
@@ -1203,6 +1214,8 @@ def run_agent_chat(cfg):
1203
1214
  "knowledge -> web_search(query).\n"
1204
1215
  "- 'read this page' / 'open this link' / 'what does <URL> say' -> fetch_url(url).\n"
1205
1216
  "- any arithmetic, percentage, or 'how much is' math -> calculate(expression).\n"
1217
+ "- a live/current NUMBER with no URL given (price, exchange rate, weather, score, "
1218
+ "stock/crypto) -> web_search(query). Do NOT guess an API URL for these.\n"
1206
1219
  "- 'create/make/generate/export a PDF' of some text/data -> create_pdf(text, title).\n"
1207
1220
  + _pdf_read_choose_line + _email_choose_line +
1208
1221
  "- A specific known API/URL was given -> http_request().\n"
@@ -1212,8 +1225,11 @@ def run_agent_chat(cfg):
1212
1225
  "If nothing works, call final_answer explaining what you need — do NOT make up an answer.\n"
1213
1226
  "- Only report what a tool ACTUALLY returned. Never fabricate a response, body, or status code.\n"
1214
1227
  "- Pass an http_request response DIRECTLY to final_answer — do NOT split, parse, or index it.\n"
1215
- "- If a response starts with 'HTTP 2' it SUCCEEDED call final_answer immediately.\n"
1216
- "- If a tool returns 'Error:' or HTTP 4xx/5xx, try a DIFFERENT approach, not the same URL.\n"
1228
+ "- When a call's result is UNCERTAIN (any network call), do NOT call final_answer "
1229
+ "in the SAME block. Call the tool alone, READ the Observation, THEN answer next step.\n"
1230
+ "- If a response starts with 'HTTP 2' it SUCCEEDED — answer with it next step.\n"
1231
+ "- NEVER pass an 'Error:' / HTTP 4xx / 5xx result to final_answer. On an error, try a "
1232
+ "DIFFERENT approach next step (e.g. web_search); only give up after a few tries.\n"
1217
1233
  "- Do NOT put final_answer outside the code block.\n\n"
1218
1234
  )
1219
1235
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1558,22 +1574,58 @@ def run_agent_chat(cfg):
1558
1574
  # literal messages array sent to /v1/chat/completions.
1559
1575
  class _LoggingModel(OpenAIServerModel):
1560
1576
  def generate(self, *args, **kwargs):
1561
- # Strip Qwen3 <think>…</think> reasoning traces from each step's reply
1562
- # BEFORE smolagents parses it for the code block / final_answer. Thinking
1563
- # stays on (better reasoning), but the trace never confuses the parser or
1564
- # leaks to the user.
1577
+ # Two safeguards on each step's reply BEFORE smolagents parses it for the
1578
+ # code block / final_answer:
1579
+ # 1. content=None guard — Qwen3 (and reasoning models generally) can return
1580
+ # message.content=None on a turn (e.g. it spent the whole completion on a
1581
+ # <think> trace and never emitted a code block). smolagents has NO null
1582
+ # guard (models.py: `content = response.choices[0].message.content`) and
1583
+ # feeds it straight into parse_code_blobs → `re` gets None →
1584
+ # "expected string or bytes-like object, got 'NoneType'" → the whole
1585
+ # loop spirals on parse errors and trips the WebSocket timeout. Coercing
1586
+ # None→"" turns that fatal crash into a normal "no code block" retry.
1587
+ # 2. strip any leftover Qwen3 <think>…</think> trace (belt-and-suspenders;
1588
+ # thinking is disabled via /no_think in _prepare_completion_kwargs, but a
1589
+ # stray trace must never reach the parser or leak to the user).
1565
1590
  msg = super().generate(*args, **kwargs)
1566
1591
  try:
1567
- if isinstance(getattr(msg, "content", None), str) and "</think>" in msg.content.lower():
1568
- cleaned = _strip_think(msg.content)
1569
- _log(f"stripped <think> trace ({len(msg.content) - len(cleaned)} chars)")
1592
+ content = getattr(msg, "content", None)
1593
+ if content is None:
1594
+ _log("model returned content=None (empty/thinking-only turn) → coercing to ''")
1595
+ msg.content = ""
1596
+ elif isinstance(content, str) and "</think>" in content.lower():
1597
+ cleaned = _strip_think(content)
1598
+ _log(f"stripped <think> trace ({len(content) - len(cleaned)} chars)")
1570
1599
  msg.content = cleaned
1571
1600
  except Exception as e: # noqa: BLE001
1572
- _log(f"think-strip error: {e}")
1601
+ _log(f"content-normalize error: {e}")
1573
1602
  return msg
1574
1603
 
1575
1604
  def _prepare_completion_kwargs(self, *args, **kwargs):
1576
1605
  ck = super()._prepare_completion_kwargs(*args, **kwargs)
1606
+ # Disable Qwen3 "thinking" for the agent loop. Live runs showed thinking-on
1607
+ # spends the whole completion budget on a <think> trace and returns
1608
+ # content=None (no code block) → parse-error spiral → WS timeout. The
1609
+ # `/no_think` soft switch (recognized by the Qwen3 chat template) forces a
1610
+ # direct Thought+code reply every step. Injected into the SYSTEM message so it
1611
+ # applies to EVERY step's request, not just the first user turn.
1612
+ try:
1613
+ msgs = ck.get("messages", []) or []
1614
+ for m in msgs:
1615
+ role = m.get("role") if isinstance(m, dict) else getattr(m, "role", None)
1616
+ if str(role).endswith("system") or role == "system":
1617
+ c = m.get("content") if isinstance(m, dict) else getattr(m, "content", None)
1618
+ if isinstance(c, str) and "/no_think" not in c:
1619
+ m["content"] = c + "\n\n/no_think"
1620
+ elif isinstance(c, list):
1621
+ for part in c:
1622
+ if isinstance(part, dict) and isinstance(part.get("text"), str) \
1623
+ and "/no_think" not in part["text"]:
1624
+ part["text"] += "\n\n/no_think"
1625
+ break
1626
+ break
1627
+ except Exception as e: # noqa: BLE001
1628
+ _log(f"/no_think inject error: {e}")
1577
1629
  try:
1578
1630
  msgs = ck.get("messages", []) or []
1579
1631
  _log(f"=== MODEL REQUEST: {len(msgs)} message(s) sent to the model ===")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.123",
3
+ "version": "1.0.125",
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",