@tiens.nguyen/gu-cli 1.0.750 → 1.0.751

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.
@@ -6948,6 +6948,35 @@ def _ws_distill_output(out: str, cap_head: int = 1200, cap_tail: int = 4000) ->
6948
6948
  + out[-cap_tail:])
6949
6949
 
6950
6950
 
6951
+ # READ CAPS SCALE WITH THE WINDOW (issue #49). A flat 3,000 chars (~750 tokens, ~60-80 lines,
6952
+ # middle cut) was right for #63's weak models with no prompt cache. Measured over 625 Claude
6953
+ # steps it cut a read in 23 of 37 turns (>=14.6% of steps), and each cut is a follow-up read:
6954
+ # d6ad7211 walked a 6,665-line file in slices for ~40 steps. With cache reads at 0.1x a bigger
6955
+ # read costs about what the extra step did, and steps are what we save. Claude Code's own Read
6956
+ # allows 25k tokens. Small or unknown windows keep 3,000 — a 16k local Ollama is unchanged.
6957
+ _OBS_CAP_FLOOR = 3000
6958
+ _OBS_CAP_READ_MAX = 24_000 # ≈ read_file_lines' own 400-line limit
6959
+ _OBS_CAP_SEARCH_MAX = 12_000 # ≈ grep_repo's 100 hits / list_dir's 300 entries
6960
+ _OBS_CAP_WINDOW_SHARE = 0.03 # one read may take ~3% of the window
6961
+ _OBS_CAPS = {"read": _OBS_CAP_FLOOR, "search": _OBS_CAP_FLOOR}
6962
+
6963
+
6964
+ def _obs_caps_for_window(window_tokens: int) -> dict:
6965
+ """{read, search} character caps for a model with this context window (0 = unknown)."""
6966
+ if not window_tokens or window_tokens <= 0:
6967
+ return {"read": _OBS_CAP_FLOOR, "search": _OBS_CAP_FLOOR}
6968
+ read = int(window_tokens * 4 * _OBS_CAP_WINDOW_SHARE) # ~4 chars per token
6969
+ read = max(_OBS_CAP_FLOOR, min(_OBS_CAP_READ_MAX, read))
6970
+ search = max(_OBS_CAP_FLOOR, min(_OBS_CAP_SEARCH_MAX, read // 2))
6971
+ return {"read": read, "search": search}
6972
+
6973
+
6974
+ def _set_obs_caps(window_tokens: int) -> dict:
6975
+ """Set this turn's caps (the worker runs one turn per process)."""
6976
+ _OBS_CAPS.update(_obs_caps_for_window(window_tokens))
6977
+ return dict(_OBS_CAPS)
6978
+
6979
+
6951
6980
  def _ws_cap_obs(text: str, cap: int = 3000) -> str:
6952
6981
  """Cap a READ tool's observation (read_file_lines / list_dir / read_text_file /
6953
6982
  grep_repo) before it's returned to the CodeAgent. Every observation stays in the
@@ -9007,6 +9036,8 @@ def run_agent_chat(cfg):
9007
9036
  _trim_state: dict = {"next_at": _TRIM_FIRST_AT_CHARS}
9008
9037
  _ctx_window = _model_context_window(coding_base_url, coding_model_id, coding_api_key)
9009
9038
  _compact_at = _compact_threshold(_ctx_window)
9039
+ _caps = _set_obs_caps(_ctx_window)
9040
+ _log(f"read caps: {_caps['read']} chars per read, {_caps['search']} per search (issue #49)")
9010
9041
  if _ctx_window:
9011
9042
  # SAY WHICH SOURCE ANSWERED. A measured window and a remembered one are not the same
9012
9043
  # claim, and a table entry presented as fact is how a stale number survives unquestioned.
@@ -10811,7 +10842,8 @@ def run_agent_chat(cfg):
10811
10842
  entries.sort()
10812
10843
  more = " …(truncated)" if len(entries) >= 300 else ""
10813
10844
  out = _ws_cap_obs(
10814
- f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more)
10845
+ f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more,
10846
+ cap=_OBS_CAPS["search"])
10815
10847
  _emit_read("Listing files", path, f"{len(entries)} file(s)")
10816
10848
  _last_obs["text"] = out
10817
10849
  return out
@@ -10850,7 +10882,7 @@ def run_agent_chat(cfg):
10850
10882
  return _rb
10851
10883
  with open(rp, "r", encoding="utf-8", errors="replace") as fh:
10852
10884
  data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
10853
- out = _ws_cap_obs(data or "(empty file)")
10885
+ out = _ws_cap_obs(data or "(empty file)", cap=_OBS_CAPS["read"])
10854
10886
  _emit_read("Reading file", path, f"{data.count(chr(10)) + 1} line(s)"
10855
10887
  if data else "empty")
10856
10888
  _last_obs["text"] = out
@@ -11509,7 +11541,8 @@ def run_agent_chat(cfg):
11509
11541
  _last_obs["text"] = msg
11510
11542
  return msg
11511
11543
  more = "\n…(capped at 100 hits)" if len(hits) >= 100 else ""
11512
- out = _ws_cap_obs(f"Matches under {root}:\n" + "\n".join(hits) + more)
11544
+ out = _ws_cap_obs(f"Matches under {root}:\n" + "\n".join(hits) + more,
11545
+ cap=_OBS_CAPS["search"])
11513
11546
  _last_obs["text"] = out
11514
11547
  return out
11515
11548
  except Exception as e: # noqa: BLE001
@@ -11595,7 +11628,7 @@ def run_agent_chat(cfg):
11595
11628
  _emit({"type": "step",
11596
11629
  "text": f"Locating symbol → {name} ({len(defs)} def, "
11597
11630
  f"{len(calls)} call, {len(tests)} test)"})
11598
- res = _ws_cap_obs("\n".join(out))
11631
+ res = _ws_cap_obs("\n".join(out), cap=_OBS_CAPS["search"])
11599
11632
  _last_obs["text"] = res
11600
11633
  return res
11601
11634
  except Exception as ex: # noqa: BLE001
@@ -11634,7 +11667,8 @@ def run_agent_chat(cfg):
11634
11667
  _last_obs["text"] = _rb
11635
11668
  return _rb
11636
11669
  body = "".join(f"{i}: {lines[i - 1]}" for i in range(s, e + 1))
11637
- out = _ws_cap_obs(f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}")
11670
+ out = _ws_cap_obs(f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}",
11671
+ cap=_OBS_CAPS["read"])
11638
11672
  _search_tally_bump("read")
11639
11673
  _emit_read("Reading lines", f"{path}:{s}-{e}", f"of {len(lines)} line(s)")
11640
11674
  _last_obs["text"] = out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.750",
3
+ "version": "1.0.751",
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",
@@ -37,6 +37,8 @@
37
37
  "test:cache-e2e": "GONEXT_OLLAMA1_LIVE=1 node --test --test-timeout=1800000 tests/prefix-cache-e2e.test.mjs",
38
38
  "test:tokens": "python3 -m unittest discover tests -p \"test_cached_tokens.py\" -v && node --test tests/prefix-cache.test.mjs tests/agent-turn-shape.test.mjs",
39
39
  "test:selection": "node --test tests/selection-scrollback.test.mjs tests/selection-while-thinking.test.mjs tests/terminal-selection.test.mjs tests/terminal-copy.test.mjs tests/terminal-viewport.test.mjs tests/hover-repaint.test.mjs",
40
+ "test:steps": "python3 scripts/py-tap.py tests/test_read_caps.py tests/test_trim_payback.py tests/test_explore.py tests/test_and_chains.py tests/test_edit_file_primary.py tests/test_bare_code_rescue.py tests/test_batching_rule.py tests/test_no_pdf_routing.py",
41
+ "test:skills": "python3 scripts/py-tap.py tests/test_skills_mcp_turn.py tests/test_hooks_turn.py tests/test_mcp_client.py && node --test tests/skills.test.mjs tests/mcp-config.test.mjs tests/hooks-config.test.mjs tests/slash-menu.test.mjs tests/conversation-archive.test.mjs",
40
42
  "capture:turn": "GONEXT_OLLAMA1_LIVE=1 node scripts/capture-turn-fixture.mjs",
41
43
  "test:bearer": "node --test tests/bearer-routing.test.mjs tests/bearer-verify.test.mjs",
42
44
  "test:bearer-e2e": "bash scripts/bearer-e2e.sh"