@tiens.nguyen/gu-cli 1.0.747 → 1.0.749
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 +82 -38
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -2140,8 +2140,34 @@ def _usage_reports_cache(usage) -> bool:
|
|
|
2140
2140
|
# against the corrected counter so that the OBSERVED GOOD BEHAVIOUR is preserved: one pass in
|
|
2141
2141
|
# 86 steps, firing where it fired before. Raising the counter without raising the threshold
|
|
2142
2142
|
# would fire earlier and more often — the nibbling this whole mechanism exists to stop.
|
|
2143
|
-
_TRIM_FIRST_AT_CHARS =
|
|
2143
|
+
_TRIM_FIRST_AT_CHARS = 90_000 # was 125k: a first trim frees most of the history, so it pays
|
|
2144
|
+
# back fast — the payback gate below keeps that honest
|
|
2144
2145
|
_TRIM_AGAIN_AFTER_CHARS = 60_000 # …and not again until it has grown this much further
|
|
2146
|
+
# A trim REWRITES the history into the prompt cache, so it is only worth it when the smaller
|
|
2147
|
+
# prompt repays that within a few steps (task #279). Prices relative to a normal input token:
|
|
2148
|
+
# Anthropic cache write 1.25x (5-minute TTL), cache read 0.1x. Measured 2026-09-25 on one turn:
|
|
2149
|
+
# trim 1 (69.7k → 28.3k tokens) paid back in ~2 steps; trim 2 (54.8k → 38.6k) needed ~15 and
|
|
2150
|
+
# only 3 followed, so it cost ~18k more than it saved.
|
|
2151
|
+
_TRIM_PAYBACK_STEPS = 5
|
|
2152
|
+
_CACHE_WRITE_X = 1.25
|
|
2153
|
+
_CACHE_READ_X = 0.1
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
def _trim_payback_steps(history_chars: int, freed_chars: int) -> float:
|
|
2157
|
+
"""How many later steps a trim needs to repay its cache cost. inf when it frees nothing.
|
|
2158
|
+
|
|
2159
|
+
After a trim, the history that remains is written to the cache once instead of read
|
|
2160
|
+
(costing WRITE - READ more per token); every later step then reads `freed` fewer tokens
|
|
2161
|
+
(saving READ each). Chars stand in for tokens — only the ratio matters.
|
|
2162
|
+
"""
|
|
2163
|
+
if freed_chars <= 0:
|
|
2164
|
+
return float("inf")
|
|
2165
|
+
remaining = max(0, history_chars - freed_chars)
|
|
2166
|
+
return ((_CACHE_WRITE_X - _CACHE_READ_X) * remaining) / (_CACHE_READ_X * freed_chars)
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
def _trim_pays_back(history_chars: int, freed_chars: int, limit: float = _TRIM_PAYBACK_STEPS) -> bool:
|
|
2170
|
+
return _trim_payback_steps(history_chars, freed_chars) <= limit
|
|
2145
2171
|
|
|
2146
2172
|
|
|
2147
2173
|
def _step_memory_chars(steps) -> int:
|
|
@@ -7994,16 +8020,24 @@ def run_agent_chat(cfg):
|
|
|
7994
8020
|
# ORDER to use them in and the CONSTRAINTS that bite — so keep those, drop the third
|
|
7995
8021
|
# retelling. Every rule below is a scar: token waste (#63/#79), no-shell/no-sudo,
|
|
7996
8022
|
# key-only deploys (#57).
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
"
|
|
8023
|
+
# Task #48 (GitHub): string edits are the default, like Claude Code's Edit tool. A
|
|
8024
|
+
# line-number edit needs a fresh read before every edit (each one shifts the numbers):
|
|
8025
|
+
# a measured turn spent 12 of its 43 steps on read → edit_lines → read → edit_lines.
|
|
8026
|
+
" THE EDIT LOOP: LOCATE (explore, grep_repo) → read_file_lines ONCE around what you will "
|
|
8027
|
+
"change (a SMALL range; never re-read the whole file or the same range twice) → make ALL "
|
|
8028
|
+
"of that file's edits in ONE block with edit_file(path, old_string, new_string), quoting "
|
|
8029
|
+
"a short UNIQUE snippet from what you read — no line numbers, so no re-read between "
|
|
8030
|
+
"edits → VERIFY with run_command (in the same block when you can). edit_lines only when "
|
|
8031
|
+
"you cannot quote a unique string (e.g. replacing a long range); create_file(overwrite=True) "
|
|
8001
8032
|
"replaces a whole file in ONE call (no read+edit just to rewrite it).\n"
|
|
8002
|
-
" EXPLORE FIRST
|
|
8003
|
-
"
|
|
8004
|
-
"
|
|
8005
|
-
"
|
|
8006
|
-
"
|
|
8033
|
+
" EXPLORE FIRST — before CHANGING code you have not seen yet in this conversation "
|
|
8034
|
+
"(always for a change that spans several files or layers — frontend + backend, UI + API), "
|
|
8035
|
+
"and for any broad question (where is X handled, how does Y work, what would a change to Z "
|
|
8036
|
+
"touch): call explore(question) ONCE to map the files and lines involved. It searches in "
|
|
8037
|
+
"a SEPARATE context and returns a short summary with path:line references, so its greps "
|
|
8038
|
+
"and reads do not pile up here and are not re-sent on every later step. Then read only "
|
|
8039
|
+
"the exact ranges you will edit. Use grep_repo/read_file_lines directly when you already "
|
|
8040
|
+
"know the file or the exact string.\n"
|
|
8007
8041
|
" run_command CHAINS: put dependent commands in ONE call with && — e.g. "
|
|
8008
8042
|
"run_command(\"cd app && ./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk\") "
|
|
8009
8043
|
"— they run in order, `cd` sets the folder for the rest, and the first failure stops the "
|
|
@@ -8043,12 +8077,11 @@ def run_agent_chat(cfg):
|
|
|
8043
8077
|
"- user asks to FIX A BUG / MODIFY / ADD CODE in a workspace -> "
|
|
8044
8078
|
"grep_repo(pattern) when you know the exact string, or rag_search_workspace(query) "
|
|
8045
8079
|
"when you do not, to FIND the file, THEN "
|
|
8046
|
-
"read_file_lines(file, around the hit)
|
|
8047
|
-
"
|
|
8080
|
+
"read_file_lines(file, around the hit) ONCE, THEN edit_file(file, old_string, new_string) "
|
|
8081
|
+
"for EACH change to that file — all in ONE block —, THEN "
|
|
8048
8082
|
"run_command('…test…') to VERIFY if run permission exists — read failures and fix, "
|
|
8049
8083
|
"iterate. Finish with final_answer summarizing WHAT you changed (files + lines). "
|
|
8050
|
-
"
|
|
8051
|
-
"stale — do not trust rag_search for freshly edited code).\n"
|
|
8084
|
+
"Do not trust rag_search for freshly edited code (the index may be stale).\n"
|
|
8052
8085
|
) if _WS_AVAILABLE else ""
|
|
8053
8086
|
_email_choose_line = (
|
|
8054
8087
|
"- 'email …' / 'send an email to …' -> send_email(to, subject, body) "
|
|
@@ -8232,7 +8265,9 @@ def run_agent_chat(cfg):
|
|
|
8232
8265
|
# the cached reference above. Workspace/coding mode only.
|
|
8233
8266
|
+ ("Remember: FINISH what was asked yourself — fix blockers, VERIFY the outcome, then "
|
|
8234
8267
|
"final_answer with evidence; never hand the task back. If it only asks to RECORD work "
|
|
8235
|
-
"(create a task, a note, a plan), recording it IS the task — do not start the work
|
|
8268
|
+
"(create a task, a note, a plan), recording it IS the task — do not start the work. "
|
|
8269
|
+
"To change code you have not seen yet — especially across several files — map it with "
|
|
8270
|
+
"ONE explore() call first, then make each file's edits in ONE block with edit_file.\n"
|
|
8236
8271
|
if _WS_AVAILABLE else "")
|
|
8237
8272
|
+ "\nTASK (answer THIS, choose the tool that fits it):\n"
|
|
8238
8273
|
f"{task_text}\n"
|
|
@@ -9142,38 +9177,47 @@ def run_agent_chat(cfg):
|
|
|
9142
9177
|
# context is not big enough for that trade to pay, so history is left ALONE and
|
|
9143
9178
|
# every step appends cleanly onto a cached prefix.
|
|
9144
9179
|
if _steps and len(_steps) > 2 and _trim_due(_mem_chars, _trim_state):
|
|
9145
|
-
|
|
9180
|
+
# PLAN FIRST, then apply only if it pays back its cache cost (task #279): a
|
|
9181
|
+
# trim rewrites the history into the cache, and one that frees little costs
|
|
9182
|
+
# more than it saves. Each edit is (object, attribute, new value, chars freed).
|
|
9183
|
+
_plan = []
|
|
9146
9184
|
for _st in _steps[:-2]:
|
|
9147
9185
|
_obs = getattr(_st, "observations", None)
|
|
9148
9186
|
if isinstance(_obs, str) and len(_obs) > 900:
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
"re-read the file/dir with a small range if you still need it]")
|
|
9154
|
-
_trim_n += 1
|
|
9155
|
-
_trim_saved += _before - len(_st.observations)
|
|
9187
|
+
_new = (_obs[:400].rstrip()
|
|
9188
|
+
+ "\n…[earlier observation trimmed to save context — "
|
|
9189
|
+
"re-read the file/dir with a small range if you still need it]")
|
|
9190
|
+
_plan.append((_st, "observations", _new, len(_obs) - len(_new)))
|
|
9156
9191
|
_mo = getattr(_st, "model_output", None)
|
|
9157
9192
|
if isinstance(_mo, str) and len(_mo) > 700:
|
|
9158
|
-
_before = len(_mo)
|
|
9159
9193
|
# Keep the Thought (everything before the code block); the code is
|
|
9160
9194
|
# redundant with the observation that already came back.
|
|
9161
9195
|
_head = re.split(r"<code[\s>]|```", _mo, maxsplit=1)[0].rstrip()
|
|
9162
9196
|
_kept = (_head[:500].rstrip() if _head else _mo[:300].rstrip())
|
|
9163
9197
|
_new = _kept + "\n…[code trimmed to save context — its result is in the Observation]"
|
|
9164
|
-
if len(_new) <
|
|
9165
|
-
_st
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
#
|
|
9169
|
-
#
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9198
|
+
if len(_new) < len(_mo):
|
|
9199
|
+
_plan.append((_st, "model_output", _new, len(_mo) - len(_new)))
|
|
9200
|
+
# The SAME code, a second time: smolagents keeps the parsed code action in
|
|
9201
|
+
# step.tool_calls and renders it as its own message on every request, so
|
|
9202
|
+
# stripping <code> from model_output above saves nothing on its own. See
|
|
9203
|
+
# _trim_tool_call_args for why trimming the duplicate is safe.
|
|
9204
|
+
for _tc in (getattr(_st, "tool_calls", None) or []):
|
|
9205
|
+
_a = getattr(_tc, "arguments", None)
|
|
9206
|
+
if isinstance(_a, str) and len(_a) > 300:
|
|
9207
|
+
_new = _a[:300].rstrip() + _CODE_TRIMMED_MARK
|
|
9208
|
+
if len(_new) < len(_a):
|
|
9209
|
+
_plan.append((_tc, "arguments", _new, len(_a) - len(_new)))
|
|
9210
|
+
_freed = sum(p[3] for p in _plan)
|
|
9211
|
+
_trim_n, _trim_saved = 0, 0
|
|
9212
|
+
if _trim_pays_back(_mem_chars, _freed):
|
|
9213
|
+
for _obj, _attr, _new, _gain in _plan:
|
|
9214
|
+
setattr(_obj, _attr, _new)
|
|
9175
9215
|
_trim_n += 1
|
|
9176
|
-
_trim_saved +=
|
|
9216
|
+
_trim_saved += _gain
|
|
9217
|
+
else:
|
|
9218
|
+
_log(f"context trim skipped: would free ~{_freed} of {_mem_chars} chars — "
|
|
9219
|
+
f"repays its cache rewrite only after ~{_trim_payback_steps(_mem_chars, _freed):.0f} "
|
|
9220
|
+
f"steps (> {_TRIM_PAYBACK_STEPS})")
|
|
9177
9221
|
# Arm the next pass off the size we actually reached, so a big win buys a
|
|
9178
9222
|
# long quiet period and a small one cannot re-fire next step.
|
|
9179
9223
|
_note_trimmed(_step_memory_chars(_steps), _trim_state)
|
|
@@ -11563,7 +11607,7 @@ def run_agent_chat(cfg):
|
|
|
11563
11607
|
|
|
11564
11608
|
@tool
|
|
11565
11609
|
def edit_lines(path: str, start_line: int, end_line: int, new_content: str) -> str:
|
|
11566
|
-
"""Replace lines start_line..end_line (inclusive, 1-based) of a workspace file with new_content.
|
|
11610
|
+
"""Replace lines start_line..end_line (inclusive, 1-based) of a workspace file with new_content. Use it only when you cannot quote a unique string for edit_file (e.g. replacing a long range). Line numbers shift after every edit, so re-read before another edit_lines in the same file. A backup is saved automatically.
|
|
11567
11611
|
|
|
11568
11612
|
Args:
|
|
11569
11613
|
path: file inside a registered workspace.
|
|
@@ -11620,7 +11664,7 @@ def run_agent_chat(cfg):
|
|
|
11620
11664
|
|
|
11621
11665
|
@tool
|
|
11622
11666
|
def edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
11623
|
-
"""
|
|
11667
|
+
"""The PRIMARY edit tool: replace an EXACT, unique text occurrence in a workspace file (byte-for-byte, incl. whitespace). It needs no line numbers, so several edit_file calls to one file go in ONE block after a single read. A backup is saved automatically.
|
|
11624
11668
|
|
|
11625
11669
|
Args:
|
|
11626
11670
|
path: file inside a registered workspace.
|
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.749",
|
|
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",
|