@tiens.nguyen/gu-cli 1.0.704 → 1.0.705

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.
@@ -1329,6 +1329,11 @@ def _render_numbered_tool_list(tools) -> str:
1329
1329
  return ("\n".join(lines) + "\n") if lines else ""
1330
1330
 
1331
1331
 
1332
+ # A step line only `gu --verbose` shows (#45): retry notices and context trimming are how the
1333
+ # agent keeps itself going, and the user can do nothing with them.
1334
+ _VERBOSE_PREFIX = "__VERBOSE__::"
1335
+
1336
+
1332
1337
  def _summarise_step(step_log):
1333
1338
  """Return a short human-readable description of an agent step."""
1334
1339
  tool_calls = getattr(step_log, "tool_calls", None) or []
@@ -1337,6 +1342,9 @@ def _summarise_step(step_log):
1337
1342
  step_num = getattr(step_log, "step_number", None)
1338
1343
 
1339
1344
  parts = []
1345
+ # A retry the agent recovers from by itself is machinery, not progress: the terminal shows
1346
+ # it only under `gu --verbose` (#45). The model still gets smolagents' exact error text.
1347
+ verbose_only = False
1340
1348
  for tc in tool_calls:
1341
1349
  name = getattr(tc, "name", "")
1342
1350
  args = getattr(tc, "arguments", None)
@@ -1404,15 +1412,18 @@ def _summarise_step(step_log):
1404
1412
  # step loop), so the model keeps the precise wording it needs to self-correct.
1405
1413
  _msg = "the model wrote no code this step — asking again"
1406
1414
  parts.append(f"→ {_msg}" if parts else _msg)
1415
+ verbose_only = True
1407
1416
  elif "code parsing failed" in err.lower() or "syntaxerror" in err.lower():
1408
1417
  # The sibling shape: there WAS a block, and its body would not compile.
1409
1418
  _msg = "the model's code wouldn't run — asking again"
1410
1419
  parts.append(f"→ {_msg}" if parts else _msg)
1420
+ verbose_only = True
1411
1421
  else:
1412
1422
  parts.append(f"→ Error: {_clip(err, 160)}")
1413
1423
 
1414
1424
  # No numeric "Step N:" prefix — show only the semantic action.
1415
- return (" | ".join(parts) if parts else "thinking…")
1425
+ text = " | ".join(parts) if parts else "thinking…"
1426
+ return f"{_VERBOSE_PREFIX}{text}" if verbose_only else text
1416
1427
 
1417
1428
 
1418
1429
  # Keywords that strongly indicate the user wants to make an HTTP/network request,
@@ -8689,7 +8700,7 @@ def run_agent_chat(cfg):
8689
8700
  # once, and that answer is remembered. The line exists so the figure is not mistaken
8690
8701
  # for something the endpoint reported — see the "measured vs remembered" note above.
8691
8702
  _emit({"type": "step", "text": (
8692
- f"context window {_ctx_window // 1000}k for {coding_model_id} (published default — "
8703
+ f"{_VERBOSE_PREFIX}context window {_ctx_window // 1000}k for {coding_model_id} (published default — "
8693
8704
  f"this endpoint does not report one). Corrects itself only if it is too HIGH; "
8694
8705
  f"/context-window to set it yourself")})
8695
8706
  else:
@@ -8705,7 +8716,7 @@ def run_agent_chat(cfg):
8705
8716
  # command immediately and cannot act on an env var without leaving, editing a file and
8706
8717
  # restarting — which is how this stayed unset long enough to matter.
8707
8718
  _emit({"type": "step", "text": (
8708
- f"context window unknown — compacting at ~{_compact_at // 1000}k tokens; "
8719
+ f"{_VERBOSE_PREFIX}context window unknown — compacting at ~{_compact_at // 1000}k tokens; "
8709
8720
  "run /context-window <tokens> to use more")})
8710
8721
 
8711
8722
  # Task #90 Phase 3 — completion gate (code-enforced, complements the prompt). Fires at
@@ -8958,7 +8969,7 @@ def run_agent_chat(cfg):
8958
8969
  if _cs and not _compact_state["told"]:
8959
8970
  _compact_state["told"] = True
8960
8971
  _emit({"type": "step", "text": (
8961
- f"Context reached ~{_in_tok // 1000}k tokens — trimming older "
8972
+ f"{_VERBOSE_PREFIX}Context reached ~{_in_tok // 1000}k tokens — trimming older "
8962
8973
  "steps to stay fast (files are re-readable)")})
8963
8974
  if _cs:
8964
8975
  _log(f"context compact: saved ~{_cs} chars "
@@ -9477,9 +9488,9 @@ def run_agent_chat(cfg):
9477
9488
  # to say. On empty-content there is genuinely nothing to show — the model spent
9478
9489
  # the turn in its reasoning channel and wrote nothing — so a short line stays.
9479
9490
  _emit({"type": "step", "text": (
9480
- "The model thought but didn't act — asking it again…"
9491
+ f"{_VERBOSE_PREFIX}The model thought but didn't act — asking it again…"
9481
9492
  if why == "empty-content"
9482
- else "Asking the model to take the action it described…")})
9493
+ else f"{_VERBOSE_PREFIX}Asking the model to take the action it described…")})
9483
9494
  retry_kwargs = dict(completion_kwargs)
9484
9495
  retry_kwargs["messages"] = list(completion_kwargs.get("messages") or []) + [
9485
9496
  {"role": "user", "content": self._CODE_NOW_DIRECTIVE}
package/gu-repl.mjs CHANGED
@@ -137,6 +137,7 @@ import { REQUIRED_AGENT_MODEL, REQUIRED_AGENT_PREFIX, REQUIRED_EMBED_MODEL } fro
137
137
  // plain SGR code: terminals disagree about colour, so accent.mjs picks 24-bit, 256 or the
138
138
  // sixteen-colour magenta from what this one actually claims to support.
139
139
  import { accent } from "./accent.mjs";
140
+ import { isNoCodeRetry, splitVerboseLine, stepHasCode } from "./verbose-lines.mjs";
140
141
  import {
141
142
  createTranscript,
142
143
  displayRows,
@@ -8466,6 +8467,16 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
8466
8467
  // 400 chars — enough for a one-line label, not for the paragraphs we now print). Bounded so a
8467
8468
  // runaway generation cannot grow without limit; the prose we keep is far shorter anyway.
8468
8469
  let stepThought = "";
8470
+ // #45: the prose of a step whose output had NO code, held back until the next line says what
8471
+ // became of it. A no-code retry notice discards it (the model restates the plan on the retry);
8472
+ // anything else prints it. Always null under --verbose, which shows everything as it comes.
8473
+ let heldThought = null;
8474
+ const releaseHeldThought = () => {
8475
+ if (heldThought === null) return;
8476
+ const raw = heldThought;
8477
+ heldThought = null;
8478
+ if (printThoughtBlock(raw, onContent)) lastWasBlank = false;
8479
+ };
8469
8480
  let hoverRow = -1; // last mouse viewport row (from motion events) → highlight when it's on the thought line
8470
8481
  let thoughtRow = -1; // absolute viewport row of the thought line (line 1), learned via \x1b[6n
8471
8482
  let lastDsrAt = 0; // throttle the cursor-position probe
@@ -8894,7 +8905,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
8894
8905
  carry += stripThinkTags(chunk);
8895
8906
  let nl;
8896
8907
  while ((nl = carry.indexOf("\n")) >= 0) {
8897
- const line = carry.slice(0, nl);
8908
+ let line = carry.slice(0, nl);
8898
8909
  carry = carry.slice(nl + 1);
8899
8910
  const alreadyFlushed = carryFlushedLen;
8900
8911
  carryFlushedLen = 0; // reset — `carry` now starts a fresh pending line
@@ -8907,6 +8918,21 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
8907
8918
  almostDone = /almost/i.test(line);
8908
8919
  continue;
8909
8920
  }
8921
+ // #45: retry notices and context-window bookkeeping are the agent's own machinery. Shown
8922
+ // under --verbose only; otherwise swallowed, together with the no-code false start the
8923
+ // retry notice is about.
8924
+ {
8925
+ const v = splitVerboseLine(line);
8926
+ if (v.verboseOnly) {
8927
+ if (!REPL_VERBOSE) {
8928
+ if (isNoCodeRetry(v.text)) heldThought = null;
8929
+ lastContentAt = Date.now();
8930
+ continue;
8931
+ }
8932
+ line = v.text;
8933
+ }
8934
+ }
8935
+ if (heldThought !== null && line.trim() !== "") releaseHeldThought();
8910
8936
  // Task #136: a RAG index file was written. Swallow the line — it belongs in the
8911
8937
  // BOTTOM BAR, not in the action list — and remember the newest one. Kept out of the
8912
8938
  // bullet stream deliberately: indexing writes a shard per batch, so printing them
@@ -8939,7 +8965,11 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
8939
8965
  // The step's model output is complete. Print its reasoning HERE, before the tools
8940
8966
  // report what they did, so the transcript reads in the order it happened: the model
8941
8967
  // says what it intends, then the actions it took appear under it.
8942
- if (printThoughtBlock(stepThought, onContent)) lastWasBlank = false;
8968
+ if (!REPL_VERBOSE && stepThought.trim() && !stepHasCode(stepThought)) {
8969
+ heldThought = stepThought; // #45: decided by the next line — see releaseHeldThought
8970
+ } else if (printThoughtBlock(stepThought, onContent)) {
8971
+ lastWasBlank = false;
8972
+ }
8943
8973
  stepThought = "";
8944
8974
  }
8945
8975
  continue;
@@ -9328,6 +9358,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
9328
9358
  lastWasBlank = false;
9329
9359
  }
9330
9360
  }
9361
+ releaseHeldThought(); // #45: nothing followed it, so it was not a false start
9331
9362
  clearStatus();
9332
9363
  // Task #104: expose this turn's code-model INPUT + the CUMULATIVE output-token
9333
9364
  // total (all turns for this user + coding server) so the caller can print a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.704",
3
+ "version": "1.0.705",
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",
@@ -60,6 +60,7 @@
60
60
  "job-pools.mjs",
61
61
  "rag-job-config.mjs",
62
62
  "completed-flush.mjs",
63
+ "verbose-lines.mjs",
63
64
  "agent-model-label.mjs",
64
65
  "coding-choice-tags.mjs",
65
66
  "coding-provider-setup.mjs",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Step lines only `gu --verbose` shows (#45).
3
+ *
4
+ * The agent keeps itself going with retries ("the model wrote no code this step — asking
5
+ * again") and context bookkeeping ("context window 128k … published default", "Context reached
6
+ * ~40k tokens — trimming older steps"). The user can do nothing with either, so the agent tags
7
+ * them with this prefix and the terminal drops them unless it runs with --verbose.
8
+ */
9
+ export const VERBOSE_PREFIX = "__VERBOSE__::";
10
+
11
+ /** Untag a line: `{ verboseOnly, text }`. Lines without the prefix come back unchanged. */
12
+ export function splitVerboseLine(line) {
13
+ const s = String(line ?? "");
14
+ return s.startsWith(VERBOSE_PREFIX)
15
+ ? { verboseOnly: true, text: s.slice(VERBOSE_PREFIX.length) }
16
+ : { verboseOnly: false, text: s };
17
+ }
18
+
19
+ /**
20
+ * The notice that the model's LAST reply carried no code at all. That reply's prose is then a
21
+ * false start: the model is asked again and restates it, so printing both shows the same plan
22
+ * twice around a notice the user does not need.
23
+ */
24
+ export function isNoCodeRetry(text) {
25
+ return /wrote no code this step|thought but didn't act|take the action it described/i.test(
26
+ String(text ?? "")
27
+ );
28
+ }
29
+
30
+ /** Did a step's raw model output contain a code block? */
31
+ export function stepHasCode(raw) {
32
+ return /<code[\s>]/i.test(String(raw ?? ""));
33
+ }