@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.
- package/gonext_agent_chat.py +45 -5
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
10469
|
-
"
|
|
10470
|
-
"
|
|
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.
|
|
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",
|