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

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"
@@ -900,11 +921,18 @@ def run_agent_chat(cfg):
900
921
  else:
901
922
  coding_base_url = agent_base_url
902
923
  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
924
+ # Multi-step ReAct loop (thinking agent): the agent may take several
925
+ # Thought tool Observation steps and then call final_answer() itself. This
926
+ # replaced the old strict single-shot (max_steps=1) once a stronger coding model
927
+ # (Qwen3-14B class) made real multi-step reasoning reliable. Default budget 8,
928
+ # overridable via cfg.maxSteps. The provide_final_answer override below is now only
929
+ # the exhaustion fallback (loop ends without final_answer).
930
+ try:
931
+ max_steps = int(cfg.get("maxSteps") or 8)
932
+ except (TypeError, ValueError):
933
+ max_steps = 8
934
+ if max_steps < 1:
935
+ max_steps = 8
908
936
 
909
937
  _log(
910
938
  f"start model={agent_model_id!r} base={agent_base_url!r} "
@@ -1136,9 +1164,12 @@ def run_agent_chat(cfg):
1136
1164
  "(previews first, then sends on confirm).\n"
1137
1165
  ) if _EMAIL_AVAILABLE else ""
1138
1166
  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"
1167
+ f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
1168
+ "brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
1169
+ "tool's result (Observation) before the next step — use it to decide what to do "
1170
+ "next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
1171
+ "enough to answer, call final_answer(<your answer>) in a code block. Prefer "
1172
+ "FEWER steps, and never repeat a tool call that already succeeded.\n\n"
1142
1173
  f"You have {_tool_count_word} tools:\n"
1143
1174
  " 1. http_request(method, url, headers='', body='', username='', password='') — "
1144
1175
  "call a SPECIFIC known API/URL.\n"
@@ -1198,8 +1229,8 @@ def run_agent_chat(cfg):
1198
1229
  # Track URLs that have already failed so we don't retry dead endpoints across steps.
1199
1230
  _failed_urls: set = set()
1200
1231
 
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).
1232
+ # Remember the last tool output so the max-steps fallback can report exactly what a
1233
+ # tool returned (no extra model call) if the loop ends without final_answer.
1203
1234
  _last_obs: dict = {"text": ""}
1204
1235
 
1205
1236
  @tool
@@ -1450,7 +1481,7 @@ def run_agent_chat(cfg):
1450
1481
 
1451
1482
  # 2) Render the Markdown to PDF bytes locally (pure-Python xhtml2pdf).
1452
1483
  # 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.
1484
+ # emoji), so the tool always returns a string and never breaks the agent loop.
1454
1485
  _emit({"type": "step", "text": "Rendering PDF…"})
1455
1486
  try:
1456
1487
  pdf_bytes = _render_pdf_bytes(markdown_text, doc_title)
@@ -1526,6 +1557,21 @@ def run_agent_chat(cfg):
1526
1557
  # and any step memory it accumulates. completion_kwargs["messages"] here is the
1527
1558
  # literal messages array sent to /v1/chat/completions.
1528
1559
  class _LoggingModel(OpenAIServerModel):
1560
+ 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.
1565
+ msg = super().generate(*args, **kwargs)
1566
+ 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)")
1570
+ msg.content = cleaned
1571
+ except Exception as e: # noqa: BLE001
1572
+ _log(f"think-strip error: {e}")
1573
+ return msg
1574
+
1529
1575
  def _prepare_completion_kwargs(self, *args, **kwargs):
1530
1576
  ck = super()._prepare_completion_kwargs(*args, **kwargs)
1531
1577
  try:
@@ -1569,21 +1615,22 @@ def run_agent_chat(cfg):
1569
1615
  else:
1570
1616
  _log("agent tool mode: code (python CodeAgent)")
1571
1617
 
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
1618
+ # Exhaustion fallback: normally the model calls final_answer() itself within the
1619
+ # multi-step loop and that answer is used. Only if it burns through all max_steps
1620
+ # WITHOUT calling final_answer does smolagents call provide_final_answer to
1574
1621
  # 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):
1622
+ # deterministically (or a plain reply) rather than dead-ending and without a weak
1623
+ # model corrupting exact tool output (dates/numbers).
1624
+ class _ToolAgent(_AgentBase):
1578
1625
  def provide_final_answer(self, task, *args, **kwargs):
1579
1626
  from smolagents.models import ChatMessage, MessageRole
1580
1627
  text = (_last_obs.get("text") or "").strip()
1581
1628
  if text:
1582
- _log(f"single-shot fallback (last tool obs) → {text[:80]}")
1629
+ _log(f"max-steps fallback (last tool obs) → {text[:80]}")
1583
1630
  else:
1584
- # No usable tool output (e.g. the single code block failed to parse).
1631
+ # No usable tool output (e.g. every step's code failed to parse).
1585
1632
  # Don't dead-end on a canned apology — answer from the conversation.
1586
- _log("single-shot fallback → plain reply over conversation")
1633
+ _log("max-steps fallback → plain reply over conversation")
1587
1634
  try:
1588
1635
  text = _plain_reply(
1589
1636
  messages, agent_base_url, agent_api_key, agent_model_id
@@ -1592,8 +1639,8 @@ def run_agent_chat(cfg):
1592
1639
  _log(f"plain-reply fallback error: {e}")
1593
1640
  text = ""
1594
1641
  if not text:
1595
- text = ("I couldn't complete that in one step. Please rephrase, or "
1596
- "give a specific URL/API to call.")
1642
+ text = ("I couldn't complete that within the step budget. Please "
1643
+ "rephrase, or give a specific URL/API to call.")
1597
1644
  return ChatMessage(role=MessageRole.ASSISTANT, content=text)
1598
1645
 
1599
1646
  try:
@@ -1622,14 +1669,13 @@ def run_agent_chat(cfg):
1622
1669
  agent_kwargs["additional_authorized_imports"] = [
1623
1670
  "json", "base64", "urllib", "urllib.request", "urllib.error"
1624
1671
  ]
1625
- agent = _SingleShotAgent(**agent_kwargs)
1672
+ agent = _ToolAgent(**agent_kwargs)
1626
1673
  with contextlib.redirect_stdout(sys.stderr):
1627
1674
  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.
1675
+ # Final formatting — NO extra summarizer model call. In multi-step mode the
1676
+ # agent's own final_answer() (or the max-steps fallback above) already holds the
1677
+ # synthesized answer; we just strip the internal hint tags we appended to tool
1678
+ # results so they don't leak to the user.
1633
1679
  _emit({"type": "step", "text": "Composing answer…"})
1634
1680
  final_text = _strip_tool_tags(str(result).strip()) or "[No result]"
1635
1681
  _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.123",
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",