@tiens.nguyen/gonext-local-worker 1.0.237 → 1.0.238

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.
@@ -1392,8 +1392,14 @@ def _format_text_for_pdf(text: str, title: str, base_url: str, api_key: str,
1392
1392
  fallback = f"# {title}\n\n{fallback}"
1393
1393
  try:
1394
1394
  from openai import OpenAI
1395
+ # timeout=20, NOT 60 (task #74): this formatting pass runs INSIDE a create_pdf
1396
+ # tool call, which itself runs under the agent's python-executor wall clock. A
1397
+ # 60s wait here could eat the whole step budget — the PDF then rendered+uploaded
1398
+ # fine but the model was told the step timed out, so it re-created the PDF from
1399
+ # scratch (3 duplicate PDFs, ~10 wasted minutes). Formatting is optional polish:
1400
+ # give it 20s, then fall back to the raw text and leave room for render+upload.
1395
1401
  client = OpenAI(base_url=base_url, api_key=api_key or "local",
1396
- max_retries=0, timeout=60)
1402
+ max_retries=0, timeout=20)
1397
1403
  system_content = (
1398
1404
  "You format raw text/data into clean Markdown for a PDF document. "
1399
1405
  "Add a single top-level '# Title', sensible headings, bullet lists, "
@@ -2961,8 +2967,15 @@ def run_agent_chat(cfg):
2961
2967
  )
2962
2968
 
2963
2969
  _emit({"type": "step", "text": "Formatting document…"})
2970
+ # CHAT model, not the coder (task #74 review): formatting raw text into
2971
+ # Markdown is a language task, and the chat model (local MLX) answers in
2972
+ # seconds where the remote coder can take minutes to first token — inside
2973
+ # the 20s format timeout that difference is "formatted" vs "always falls
2974
+ # back to raw text". Same split as routing/plain replies/summarization.
2964
2975
  markdown_text = _format_text_for_pdf(
2965
- doc_text, doc_title, coding_base_url, agent_api_key, coding_model_id,
2976
+ doc_text, doc_title,
2977
+ agent_base_url or coding_base_url, agent_api_key,
2978
+ agent_model_id or coding_model_id,
2966
2979
  instruction=edit_instruction,
2967
2980
  )
2968
2981
 
@@ -3223,6 +3236,8 @@ def run_agent_chat(cfg):
3223
3236
  f"- RESEARCH BUDGET — you may make at most {research_budget} web_search/fetch_url calls "
3224
3237
  "TOTAL per task (rewording a query still counts). Reserve your remaining steps for "
3225
3238
  "create_pdf / final_answer.\n"
3239
+ "- If an earlier Observation already says '✅ Your PDF … is ready', the PDF EXISTS — "
3240
+ "do NOT call create_pdf again. Pass that download link to final_answer.\n"
3226
3241
  "- Do NOT put final_answer outside the code block.\n\n"
3227
3242
  )
3228
3243
  # Recover an interrupted prior turn's step trace, if one survives on disk (crash/
@@ -3595,6 +3610,22 @@ def run_agent_chat(cfg):
3595
3610
  _last_obs["text"] = result
3596
3611
  return result
3597
3612
 
3613
+ # Task #74 retry guard. When a create_pdf call outlives the executor's wall clock,
3614
+ # the tool still finishes (render + S3 upload succeed, "PDF ready" is emitted) but
3615
+ # the model is told the step FAILED — so it calls create_pdf again for the same
3616
+ # document. Remember the last success here; `timeout_pending` is flipped by
3617
+ # step_callback whenever a step's result was an executor-timeout error, meaning
3618
+ # whatever succeeded during that step was never reported to the model.
3619
+ _pdf_state: dict = {"last": None, "timeout_pending": False}
3620
+
3621
+ def _pdf_sig(title: str, text: str) -> str:
3622
+ return (title.strip().lower() + "||"
3623
+ + re.sub(r"\s+", " ", (text or "").strip().lower())[:1500])
3624
+
3625
+ def _pdf_titles_similar(a: str, b: str) -> bool:
3626
+ a, b = a.strip().lower(), b.strip().lower()
3627
+ return bool(a and b) and (a == b or a in b or b in a)
3628
+
3598
3629
  @tool
3599
3630
  def create_pdf(text: str, title: str = "") -> str:
3600
3631
  """Create a PDF document from text/data and return a download link.
@@ -3608,10 +3639,28 @@ def run_agent_chat(cfg):
3608
3639
  title: Optional document title shown at the top and used in the file name.
3609
3640
  """
3610
3641
  doc_title = (title or "").strip() or "Document"
3642
+ # Retry guard (task #74): identical content is ALWAYS a retry; a merely similar
3643
+ # title counts only when the previous success was swallowed by an executor
3644
+ # timeout (the model rewords/shortens on retry, so the text rarely matches).
3645
+ prev = _pdf_state.get("last")
3646
+ if prev and (
3647
+ prev["sig"] == _pdf_sig(doc_title, text)
3648
+ or (_pdf_state.get("timeout_pending")
3649
+ and _pdf_titles_similar(prev["title"], doc_title))
3650
+ ):
3651
+ _log(f"create_pdf retry detected (prev title={prev['title']!r}) "
3652
+ "→ returning the existing PDF instead of re-rendering")
3653
+ _emit({"type": "step",
3654
+ "text": f"PDF already created → {prev['title']}.pdf (reusing link)"})
3655
+ _last_obs["text"] = prev["msg"]
3656
+ return prev["msg"]
3611
3657
  # 1) Format the raw text into clean Markdown (extra model call, intentional).
3658
+ # CHAT model, not the coder — see the fast-path call site for why (task #74).
3612
3659
  _emit({"type": "step", "text": "Formatting document…"})
3613
3660
  markdown_text = _format_text_for_pdf(
3614
- text or "", doc_title, coding_base_url, agent_api_key, coding_model_id
3661
+ text or "", doc_title,
3662
+ agent_base_url or coding_base_url, agent_api_key,
3663
+ agent_model_id or coding_model_id,
3615
3664
  )
3616
3665
 
3617
3666
  # 2) Render the Markdown to PDF bytes locally (pure-Python xhtml2pdf).
@@ -3655,6 +3704,9 @@ def run_agent_chat(cfg):
3655
3704
  )
3656
3705
  _emit({"type": "step", "text": f"PDF ready → {doc_title}.pdf"})
3657
3706
  _log(f"create_pdf ok title={doc_title!r} bytes={len(pdf_bytes)}")
3707
+ _pdf_state["last"] = {
3708
+ "sig": _pdf_sig(doc_title, text), "title": doc_title, "msg": out,
3709
+ }
3658
3710
  _last_obs["text"] = out
3659
3711
  return out
3660
3712
 
@@ -3670,6 +3722,19 @@ def run_agent_chat(cfg):
3670
3722
  def step_callback(step_log):
3671
3723
  step_num = getattr(step_log, "step_number", "?")
3672
3724
 
3725
+ # Task #74: an executor-timeout error means whatever the step's tool call
3726
+ # achieved was NEVER reported to the model (a create_pdf may have finished —
3727
+ # or may still be finishing — in the background). Flag it so a repeat
3728
+ # create_pdf with a similar title is treated as the retry it is; any step
3729
+ # that ends with a real (non-timeout) result clears the flag.
3730
+ try:
3731
+ _err = getattr(step_log, "error", None)
3732
+ _pdf_state["timeout_pending"] = bool(
3733
+ _err and "maximum execution time" in str(_err)
3734
+ )
3735
+ except Exception: # noqa: BLE001
3736
+ pass
3737
+
3673
3738
  # (B) Trim OLD, LARGE tool observations out of the CodeAgent's step memory so
3674
3739
  # they aren't re-sent to the model on every subsequent step (the #63 O(n²) token
3675
3740
  # blow-up: an uncapped read_file_lines dump cost ~5-6k tokens PER remaining step).
@@ -5122,7 +5187,15 @@ def run_agent_chat(cfg):
5122
5187
  )
5123
5188
  if _AgentBase is CodeAgent:
5124
5189
  # Only the CodeAgent runs a Python executor; ToolCallingAgent takes neither.
5125
- agent_kwargs["executor_kwargs"] = {"timeout_seconds": 60}
5190
+ # 660s, NOT 60 (task #74): the wall clock must exceed the LARGEST tool-internal
5191
+ # timeout (run_command caps at 600s; create_pdf's format+render+upload can pass
5192
+ # 60s), otherwise a tool that eventually SUCCEEDS gets reported to the model as
5193
+ # 'Code execution exceeded the maximum execution time' — the model then retries
5194
+ # work that already finished (the duplicate-PDF bug). Every tool enforces its
5195
+ # own tighter timeout and returns a clean message; runaway pure-python is still
5196
+ # caught by smolagents' op-count caps (MAX_OPERATIONS/MAX_WHILE_ITERATIONS),
5197
+ # so this wall clock is a last-resort backstop, not the primary guard.
5198
+ agent_kwargs["executor_kwargs"] = {"timeout_seconds": 660}
5126
5199
  agent_kwargs["additional_authorized_imports"] = [
5127
5200
  "json", "base64", "urllib", "urllib.request", "urllib.error"
5128
5201
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.237",
3
+ "version": "1.0.238",
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",