@tiens.nguyen/gonext-local-worker 1.0.121 → 1.0.124

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 ?? 5,
1509
+ maxSteps: payload?.maxSteps ?? 8,
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 # default 10
14
+ "maxSteps": int # multi-step ReAct budget; default 8
15
15
  }
16
16
 
17
17
  Emits NDJSON lines on stdout:
@@ -570,6 +570,27 @@ def _strip_tool_tags(text: str) -> str:
570
570
  return "\n".join(out).strip()
571
571
 
572
572
 
573
+ _THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
574
+
575
+
576
+ def _strip_think(text: str) -> str:
577
+ """Remove Qwen3-style <think>…</think> reasoning traces from a model reply.
578
+
579
+ Reasoning models (e.g. Qwen3) emit their chain-of-thought inside <think> tags
580
+ before the actual answer/code. We keep thinking ON (it improves the output) but
581
+ strip the trace before smolagents parses the step, so the code/final_answer parser
582
+ only sees the real content — and the trace never leaks to the user. Handles a
583
+ dangling </think> too (some chat templates open <think> server-side)."""
584
+ if not text:
585
+ return text
586
+ text = _THINK_BLOCK.sub("", text)
587
+ low = text.lower()
588
+ if "</think>" in low and "<think>" not in low:
589
+ # Template opened the think tag for the model; drop everything up to the close.
590
+ text = text[low.find("</think>") + len("</think>"):]
591
+ return text.strip()
592
+
593
+
573
594
  _PDF_INSTALL_HINT = (
574
595
  "PDF engine not installed on the worker. On macOS:\n"
575
596
  " brew install pango libffi\n"
@@ -883,28 +904,48 @@ def run_agent_chat(cfg):
883
904
  raw_coding_base = (cfg.get("codingBaseURL") or "").strip()
884
905
  raw_coding_model = (cfg.get("codingModelId") or "").strip()
885
906
  if raw_coding_base:
886
- # A dedicated coding server is configured. If no model name was given,
887
- # ask the server which model it serves (mlx_lm.server otherwise tries to
888
- # download a mismatched name from HF and 404s).
889
- detected = raw_coding_model or _detect_model_id(raw_coding_base, agent_api_key)
890
- if detected:
891
- coding_base_url = raw_coding_base
892
- coding_model_id = detected
893
- 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.
894
913
  _log(
895
- f"coding model id unresolved for {raw_coding_base!r}; "
896
- "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)"
897
916
  )
898
917
  coding_base_url = agent_base_url
899
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
900
934
  else:
901
935
  coding_base_url = agent_base_url
902
936
  coding_model_id = agent_model_id
903
- # Strict single-shot: exactly ONE agent model call per message. The single code
904
- # block must call a tool AND final_answer together no multi-step ReAct loop.
905
- # If the model fails to call final_answer, the max-steps fallback below returns
906
- # the last tool observation deterministically (no extra model call).
907
- max_steps = 1
937
+ # Multi-step ReAct loop (thinking agent): the agent may take several
938
+ # Thought tool Observation steps and then call final_answer() itself. This
939
+ # replaced the old strict single-shot (max_steps=1) once a stronger coding model
940
+ # (Qwen3-14B class) made real multi-step reasoning reliable. Default budget 8,
941
+ # overridable via cfg.maxSteps. The provide_final_answer override below is now only
942
+ # the exhaustion fallback (loop ends without final_answer).
943
+ try:
944
+ max_steps = int(cfg.get("maxSteps") or 8)
945
+ except (TypeError, ValueError):
946
+ max_steps = 8
947
+ if max_steps < 1:
948
+ max_steps = 8
908
949
 
909
950
  _log(
910
951
  f"start model={agent_model_id!r} base={agent_base_url!r} "
@@ -1136,9 +1177,12 @@ def run_agent_chat(cfg):
1136
1177
  "(previews first, then sends on confirm).\n"
1137
1178
  ) if _EMAIL_AVAILABLE else ""
1138
1179
  tool_hint = (
1139
- "YOU HAVE EXACTLY ONE TURN. Read the TASK above. In a single code block, call "
1140
- "the ONE tool that fits THAT task, then pass its result to final_answer(). "
1141
- "Do not plan multiple steps.\n\n"
1180
+ f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
1181
+ "brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
1182
+ "tool's result (Observation) before the next step — use it to decide what to do "
1183
+ "next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
1184
+ "enough to answer, call final_answer(<your answer>) in a code block. Prefer "
1185
+ "FEWER steps, and never repeat a tool call that already succeeded.\n\n"
1142
1186
  f"You have {_tool_count_word} tools:\n"
1143
1187
  " 1. http_request(method, url, headers='', body='', username='', password='') — "
1144
1188
  "call a SPECIFIC known API/URL.\n"
@@ -1156,15 +1200,13 @@ def run_agent_chat(cfg):
1156
1200
  "\n"
1157
1201
  "http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
1158
1202
  "\n"
1159
- "BASIC AUTH — ALWAYS use username= and password=, NEVER construct headers manually:\n"
1160
- " response = http_request('GET', 'https://api.example.com/data',\n"
1161
- " username='alice@example.com', password='secret123')\n"
1162
- " final_answer(response)\n"
1163
- "The function handles base64 encoding automatically. NEVER write 'Basic ' + anything.\n"
1164
- "\n"
1165
- "BEARER TOKEN use headers:\n"
1166
- " response = http_request('GET', url, headers='{\"Authorization\": \"Bearer TOKEN\"}')\n"
1167
- " 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"
1168
1210
  "\n"
1169
1211
  "CHOOSING A TOOL (match the TASK, not these examples):\n"
1170
1212
  "- ONLY a date/time question (e.g. 'what is the date today') -> get_current_datetime().\n"
@@ -1172,6 +1214,8 @@ def run_agent_chat(cfg):
1172
1214
  "knowledge -> web_search(query).\n"
1173
1215
  "- 'read this page' / 'open this link' / 'what does <URL> say' -> fetch_url(url).\n"
1174
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"
1175
1219
  "- 'create/make/generate/export a PDF' of some text/data -> create_pdf(text, title).\n"
1176
1220
  + _pdf_read_choose_line + _email_choose_line +
1177
1221
  "- A specific known API/URL was given -> http_request().\n"
@@ -1181,8 +1225,11 @@ def run_agent_chat(cfg):
1181
1225
  "If nothing works, call final_answer explaining what you need — do NOT make up an answer.\n"
1182
1226
  "- Only report what a tool ACTUALLY returned. Never fabricate a response, body, or status code.\n"
1183
1227
  "- Pass an http_request response DIRECTLY to final_answer — do NOT split, parse, or index it.\n"
1184
- "- If a response starts with 'HTTP 2' it SUCCEEDED call final_answer immediately.\n"
1185
- "- 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"
1186
1233
  "- Do NOT put final_answer outside the code block.\n\n"
1187
1234
  )
1188
1235
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1198,8 +1245,8 @@ def run_agent_chat(cfg):
1198
1245
  # Track URLs that have already failed so we don't retry dead endpoints across steps.
1199
1246
  _failed_urls: set = set()
1200
1247
 
1201
- # Remember the last tool output so the single-shot fallback + the deterministic
1202
- # final formatting can report exactly what a tool returned (no extra model call).
1248
+ # Remember the last tool output so the max-steps fallback can report exactly what a
1249
+ # tool returned (no extra model call) if the loop ends without final_answer.
1203
1250
  _last_obs: dict = {"text": ""}
1204
1251
 
1205
1252
  @tool
@@ -1450,7 +1497,7 @@ def run_agent_chat(cfg):
1450
1497
 
1451
1498
  # 2) Render the Markdown to PDF bytes locally (pure-Python xhtml2pdf).
1452
1499
  # Catch EVERYTHING (xhtml2pdf can raise arbitrary errors on exotic glyphs /
1453
- # emoji), so the tool always returns a string and never breaks the single shot.
1500
+ # emoji), so the tool always returns a string and never breaks the agent loop.
1454
1501
  _emit({"type": "step", "text": "Rendering PDF…"})
1455
1502
  try:
1456
1503
  pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
@@ -1526,6 +1573,21 @@ def run_agent_chat(cfg):
1526
1573
  # and any step memory it accumulates. completion_kwargs["messages"] here is the
1527
1574
  # literal messages array sent to /v1/chat/completions.
1528
1575
  class _LoggingModel(OpenAIServerModel):
1576
+ def generate(self, *args, **kwargs):
1577
+ # Strip Qwen3 <think>…</think> reasoning traces from each step's reply
1578
+ # BEFORE smolagents parses it for the code block / final_answer. Thinking
1579
+ # stays on (better reasoning), but the trace never confuses the parser or
1580
+ # leaks to the user.
1581
+ msg = super().generate(*args, **kwargs)
1582
+ try:
1583
+ if isinstance(getattr(msg, "content", None), str) and "</think>" in msg.content.lower():
1584
+ cleaned = _strip_think(msg.content)
1585
+ _log(f"stripped <think> trace ({len(msg.content) - len(cleaned)} chars)")
1586
+ msg.content = cleaned
1587
+ except Exception as e: # noqa: BLE001
1588
+ _log(f"think-strip error: {e}")
1589
+ return msg
1590
+
1529
1591
  def _prepare_completion_kwargs(self, *args, **kwargs):
1530
1592
  ck = super()._prepare_completion_kwargs(*args, **kwargs)
1531
1593
  try:
@@ -1569,21 +1631,22 @@ def run_agent_chat(cfg):
1569
1631
  else:
1570
1632
  _log("agent tool mode: code (python CodeAgent)")
1571
1633
 
1572
- # Single-shot agent: if the one model call doesn't end in final_answer(),
1573
- # smolagents would normally make an EXTRA model call (provide_final_answer) to
1634
+ # Exhaustion fallback: normally the model calls final_answer() itself within the
1635
+ # multi-step loop and that answer is used. Only if it burns through all max_steps
1636
+ # WITHOUT calling final_answer does smolagents call provide_final_answer to
1574
1637
  # synthesize one. We override that to return the last tool observation
1575
- # deterministically keeping the agent to EXACTLY ONE model call, and never
1576
- # corrupting exact tool output (dates/numbers) the way a weak model would.
1577
- class _SingleShotAgent(_AgentBase):
1638
+ # deterministically (or a plain reply) rather than dead-ending and without a weak
1639
+ # model corrupting exact tool output (dates/numbers).
1640
+ class _ToolAgent(_AgentBase):
1578
1641
  def provide_final_answer(self, task, *args, **kwargs):
1579
1642
  from smolagents.models import ChatMessage, MessageRole
1580
1643
  text = (_last_obs.get("text") or "").strip()
1581
1644
  if text:
1582
- _log(f"single-shot fallback (last tool obs) → {text[:80]}")
1645
+ _log(f"max-steps fallback (last tool obs) → {text[:80]}")
1583
1646
  else:
1584
- # No usable tool output (e.g. the single code block failed to parse).
1647
+ # No usable tool output (e.g. every step's code failed to parse).
1585
1648
  # Don't dead-end on a canned apology — answer from the conversation.
1586
- _log("single-shot fallback → plain reply over conversation")
1649
+ _log("max-steps fallback → plain reply over conversation")
1587
1650
  try:
1588
1651
  text = _plain_reply(
1589
1652
  messages, agent_base_url, agent_api_key, agent_model_id
@@ -1592,8 +1655,8 @@ def run_agent_chat(cfg):
1592
1655
  _log(f"plain-reply fallback error: {e}")
1593
1656
  text = ""
1594
1657
  if not text:
1595
- text = ("I couldn't complete that in one step. Please rephrase, or "
1596
- "give a specific URL/API to call.")
1658
+ text = ("I couldn't complete that within the step budget. Please "
1659
+ "rephrase, or give a specific URL/API to call.")
1597
1660
  return ChatMessage(role=MessageRole.ASSISTANT, content=text)
1598
1661
 
1599
1662
  try:
@@ -1622,14 +1685,13 @@ def run_agent_chat(cfg):
1622
1685
  agent_kwargs["additional_authorized_imports"] = [
1623
1686
  "json", "base64", "urllib", "urllib.request", "urllib.error"
1624
1687
  ]
1625
- agent = _SingleShotAgent(**agent_kwargs)
1688
+ agent = _ToolAgent(**agent_kwargs)
1626
1689
  with contextlib.redirect_stdout(sys.stderr):
1627
1690
  result = agent.run(task_with_hint)
1628
- # Deterministic final formatting — NO summarizer model call. The agent's
1629
- # final_answer (or the single-shot fallback above) already holds exact tool
1630
- # output; we just strip the internal hint tags we appended to tool results so
1631
- # they don't leak to the user. This permanently fixes the date-corruption a
1632
- # weak summarizer model used to introduce.
1691
+ # Final formatting — NO extra summarizer model call. In multi-step mode the
1692
+ # agent's own final_answer() (or the max-steps fallback above) already holds the
1693
+ # synthesized answer; we just strip the internal hint tags we appended to tool
1694
+ # results so they don't leak to the user.
1633
1695
  _emit({"type": "step", "text": "Composing answer…"})
1634
1696
  final_text = _strip_tool_tags(str(result).strip()) or "[No result]"
1635
1697
  _log(f"done (deterministic, no summarizer call): {len(final_text)} chars")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.121",
3
+ "version": "1.0.124",
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",