@tiens.nguyen/gu-cli 1.0.749 → 1.0.750

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.
@@ -3813,6 +3813,12 @@ def _normalize_code_tags(text: str) -> str:
3813
3813
  <code> tag is present, so we don't touch backticks inside a real <code> block).
3814
3814
  5. opened-but-never-closed (exactly one "<code>", no "</code>"): append "</code>"
3815
3815
  so the parser can still extract the code instead of failing the whole step.
3816
+ 6. NO DELIMITERS AT ALL, but the reply is Python that calls something — an optional
3817
+ "Thought:" line, then e.g. `print(read_file_lines(...))`. Measured on Claude: 13
3818
+ such replies in one turn, each thrown away as "no runnable code block" and re-asked,
3819
+ about a quarter of the turn's steps. Wrapped only when the code (after the Thought
3820
+ line) COMPILES and contains a call — prose does not parse as Python, so a sentence
3821
+ is never mistaken for code.
3816
3822
  Idempotent — already-correct "<code>…</code>" is returned unchanged."""
3817
3823
  if not text:
3818
3824
  return text
@@ -3823,9 +3829,36 @@ def _normalize_code_tags(text: str) -> str:
3823
3829
  fixed = _MD_FENCE_BLOCK.sub(lambda m: f"<code>\n{m.group(1)}</code>", fixed, count=1)
3824
3830
  if fixed.count("<code>") == 1 and "</code>" not in fixed:
3825
3831
  fixed = fixed + "\n</code>"
3832
+ if "<code>" not in fixed and "</code>" not in fixed:
3833
+ bare = _bare_code_block(fixed)
3834
+ if bare is not None:
3835
+ thought, code = bare
3836
+ fixed = (thought + "\n" if thought else "") + "<code>\n" + code + "\n</code>"
3826
3837
  return fixed
3827
3838
 
3828
3839
 
3840
+ def _bare_code_block(text: str):
3841
+ """(thought, code) when `text` is Python with no delimiters — an optional leading
3842
+ "Thought: …" line, then code that compiles and calls something. Else None."""
3843
+ import ast
3844
+ t = (text or "").strip()
3845
+ if not t:
3846
+ return None
3847
+ thought = ""
3848
+ lines = t.splitlines()
3849
+ if lines and lines[0].lstrip().lower().startswith("thought:"):
3850
+ thought, t = lines[0].strip(), "\n".join(lines[1:]).strip()
3851
+ if not t:
3852
+ return None
3853
+ try:
3854
+ tree = ast.parse(t)
3855
+ except SyntaxError:
3856
+ return None
3857
+ if not any(isinstance(n, ast.Call) for n in ast.walk(tree)):
3858
+ return None
3859
+ return thought, t
3860
+
3861
+
3829
3862
  def _escape_raw_newlines_in_strings(code: str, defuse_tags: bool = False) -> str:
3830
3863
  """Turn RAW newlines/tabs that sit INSIDE a single/double-quoted string literal into
3831
3864
  \\n/\\t escapes. Triple-quoted strings, comments, and everything outside a string are
@@ -10310,7 +10343,11 @@ def run_agent_chat(cfg):
10310
10343
  r"send_email)\s*\("
10311
10344
  )
10312
10345
  fence_is_toolcall = any(re.search(_tool_call_re, b) for b in fence_bodies)
10313
- if has_code_tag or fence_is_toolcall:
10346
+ # Bare Python with no delimiters at all (case 6 of _normalize_code_tags) —
10347
+ # it used to fall through to the prose branch and cost a whole step.
10348
+ bare_code = (not has_code_tag and not fence_bodies
10349
+ and _bare_code_block(content) is not None)
10350
+ if has_code_tag or fence_is_toolcall or bare_code:
10314
10351
  normalized = _normalize_code_tags(content)
10315
10352
  if normalized != content:
10316
10353
  _log("normalized code-block tags (<code >/fence → <code>)")
@@ -10462,13 +10499,16 @@ def run_agent_chat(cfg):
10462
10499
  # from the prompt, not writing code. Deliberately carries NO literal delimiter (the
10463
10500
  # system prompt already shows the format with a worked example), so there is nothing
10464
10501
  # to echo. Applied only when kind=openai; Ollama keeps the stock message.
10502
+ # STRONG MODELS (2026-09-25): the delimiters are named again. Leaving them out was a
10503
+ # workaround for Kimi echoing them; on Claude it did the opposite — told to use "the
10504
+ # delimiters shown in the system prompt", it replied with bare code, which failed the
10505
+ # same way, 13 times in one turn. And "call exactly one tool" contradicted batching.
10465
10506
  _PARSE_ERROR_REPLACEMENT = (
10466
10507
  "Error: your last reply contained no runnable code block, so nothing executed "
10467
10508
  "and no observation was produced.\n"
10468
- "Reply with ONE 'Thought:' line, then the code block using the exact opening and "
10469
- "closing delimiters shown in the system prompt, with ONLY Python between them — "
10470
- "no prose inside the block, and do not repeat these instructions back to me. "
10471
- "Call exactly one tool, or call final_answer(...) if you are done."
10509
+ "Reply with ONE 'Thought:' line, then your Python between <code> and </code> — "
10510
+ "nothing else inside the block. Batch independent calls as usual, or call "
10511
+ "final_answer(...) if you are done."
10472
10512
  )
10473
10513
 
10474
10514
  def _sanitize_parse_errors(self, msgs):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.749",
3
+ "version": "1.0.750",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",