@tiens.nguyen/gu-cli 1.0.817 → 1.0.818
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 +16 -23
- package/gu-repl.mjs +37 -12
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -10556,11 +10556,18 @@ def run_agent_chat(cfg):
|
|
|
10556
10556
|
"run /context-window <tokens> to use more")})
|
|
10557
10557
|
|
|
10558
10558
|
# Task #90 Phase 3 — completion gate (code-enforced, complements the prompt). Fires at
|
|
10559
|
-
# most ONCE per turn: refuse the first final_answer that
|
|
10560
|
-
#
|
|
10561
|
-
# a verify-or-explain nudge fed back to the model (smolagents final_answer_checks: a
|
|
10559
|
+
# most ONCE per turn: refuse the first final_answer that hands the task back to the user,
|
|
10560
|
+
# with a verify-or-explain nudge fed back to the model (smolagents final_answer_checks: a
|
|
10562
10561
|
# raised message becomes the check-failure the model sees and retries on). After one
|
|
10563
10562
|
# nudge it ACCEPTS unconditionally — no loop; an honest "here's what failed" is valid.
|
|
10563
|
+
#
|
|
10564
|
+
# IT NO LONGER FIRES ON "THE LAST COMMAND EXITED 0" (GitHub #136). That trigger read the
|
|
10565
|
+
# truthful-exit label every successful run_command carries as proof of unverified work, so
|
|
10566
|
+
# it rejected the answer of any turn that ENDED on a command — measured over 296 real
|
|
10567
|
+
# turns: 140 rejections (47% of turns), at least 65 of them straight after the test run or
|
|
10568
|
+
# read-only check that WAS the verification, the rest mostly after a commit or a posted
|
|
10569
|
+
# comment. Each cost a ritual re-check plus a rewritten answer: 400 steps, 11.6% of all
|
|
10570
|
+
# input tokens. The label still sits in the Observation, where the model reads it.
|
|
10564
10571
|
_completion_gate: dict = {"nudged": False}
|
|
10565
10572
|
# Punt phrases: handing the CORE task back ("…start it yourself", "you'll need to run").
|
|
10566
10573
|
# Deliberately narrow so a helpful tip ("you can also run npm test") does NOT trigger it.
|
|
@@ -10586,31 +10593,17 @@ def run_agent_chat(cfg):
|
|
|
10586
10593
|
|
|
10587
10594
|
def _final_answer_gate(final_answer, memory, agent=None):
|
|
10588
10595
|
"""smolagents final_answer_check. True = accept; raising = reject with the message
|
|
10589
|
-
the model then sees. Only nudges when
|
|
10596
|
+
the model then sees. Only nudges when the answer hands the job back to the user."""
|
|
10590
10597
|
if _completion_gate["nudged"]:
|
|
10591
10598
|
return True # already nudged once this turn — accept, never loop
|
|
10592
10599
|
ans = str(final_answer if final_answer is not None else "")
|
|
10593
|
-
|
|
10594
|
-
# The most-recent tool result: a command that finished but whose EFFECT was never
|
|
10595
|
-
# verified (Phase-1 'EXITED 0 (…VERIFY…)' marker), with no server/HTTP confirmation.
|
|
10596
|
-
last_unverified = False
|
|
10597
|
-
try:
|
|
10598
|
-
for _st in reversed(getattr(memory, "steps", []) or []):
|
|
10599
|
-
_obs = getattr(_st, "observations", None)
|
|
10600
|
-
if isinstance(_obs, str) and _obs.strip():
|
|
10601
|
-
last_unverified = ("EXITED 0 (" in _obs) and not any(
|
|
10602
|
-
k in _obs for k in
|
|
10603
|
-
("RUNNING:", "listening", "HTTP 2", "http://localhost"))
|
|
10604
|
-
break
|
|
10605
|
-
except Exception: # noqa: BLE001
|
|
10606
|
-
pass
|
|
10607
|
-
if punt or last_unverified:
|
|
10600
|
+
if _PUNT_RE.search(ans):
|
|
10608
10601
|
_completion_gate["nudged"] = True
|
|
10609
10602
|
raise ValueError(
|
|
10610
|
-
"NOT DONE YET —
|
|
10611
|
-
"
|
|
10612
|
-
" 1) VERIFY it —
|
|
10613
|
-
"confirm the change took — and answer WITH that evidence; OR\n"
|
|
10603
|
+
"NOT DONE YET — your answer hands the work back to the user. Do ONE of these, "
|
|
10604
|
+
"then call final_answer:\n"
|
|
10605
|
+
" 1) DO it yourself with your tools, VERIFY it — run it, run the test, or "
|
|
10606
|
+
"re-read the file to confirm the change took — and answer WITH that evidence; OR\n"
|
|
10614
10607
|
" 2) if it genuinely cannot be done with these tools, state plainly WHAT "
|
|
10615
10608
|
"failed and WHY.\n"
|
|
10616
10609
|
"Do NOT hand an unfinished task back to the user.")
|
package/gu-repl.mjs
CHANGED
|
@@ -11057,14 +11057,23 @@ const answerFrom = (s) =>
|
|
|
11057
11057
|
const THOUGHT_PREVIEW_LINES = 3; // #134: dim lines always shown under the cut purple thought
|
|
11058
11058
|
const THOUGHT_OPEN_LINES = 8; // …and after a click / Ctrl+T
|
|
11059
11059
|
const wrapThought = (s, width, maxLines) => {
|
|
11060
|
-
const
|
|
11060
|
+
const W = Math.max(4, width);
|
|
11061
|
+
// A single token wider than a row (a URL, a path) is the only thing that MUST be split; it is
|
|
11062
|
+
// chunked to the row width so no line ever exceeds `width` (a wider row wraps physically and
|
|
11063
|
+
// breaks the redraw's row maths, #109/#138).
|
|
11064
|
+
const words = [];
|
|
11065
|
+
for (const w0 of String(s || "").split(/\s+/).filter(Boolean)) {
|
|
11066
|
+
let w = w0;
|
|
11067
|
+
while (w.length > W) { words.push(w.slice(0, W)); w = w.slice(W); }
|
|
11068
|
+
words.push(w);
|
|
11069
|
+
}
|
|
11061
11070
|
const lines = [];
|
|
11062
11071
|
let cur = "";
|
|
11063
11072
|
let truncated = false;
|
|
11064
11073
|
for (let i = 0; i < words.length; i++) {
|
|
11065
11074
|
const w = words[i];
|
|
11066
11075
|
const next = cur ? cur + " " + w : w;
|
|
11067
|
-
if (next.length <=
|
|
11076
|
+
if (next.length <= W) {
|
|
11068
11077
|
cur = next;
|
|
11069
11078
|
} else {
|
|
11070
11079
|
if (cur) lines.push(cur);
|
|
@@ -11075,15 +11084,30 @@ const wrapThought = (s, width, maxLines) => {
|
|
|
11075
11084
|
if (cur && lines.length < maxLines) lines.push(cur);
|
|
11076
11085
|
else if (cur) truncated = true;
|
|
11077
11086
|
if (truncated && lines.length) {
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11087
|
+
// End on a WHOLE, meaningful word (#134): drop whole words until the "…" fits, then drop a
|
|
11088
|
+
// dangling connective ("the", "of", "and"…) and trailing punctuation, so the line never
|
|
11089
|
+
// ends "…and the…" or "word,…". Only a lone oversized word is ever cut inside itself.
|
|
11090
|
+
const idx0 = lines.length - 1;
|
|
11091
|
+
const first = lines[idx0].split(" ")[0];
|
|
11092
|
+
const dangling = /^(a|an|the|of|to|and|or|but|in|on|at|for|with|by|from|as|is|are|that|which|if|then|than|so|into|via|per)$/i;
|
|
11093
|
+
const isDangling = (w) => dangling.test(w.replace(/[^\w]/g, ""));
|
|
11094
|
+
let idx = idx0;
|
|
11095
|
+
let ws = lines[idx].split(" ");
|
|
11096
|
+
for (;;) {
|
|
11097
|
+
while (ws.length > 0 && isDangling(ws[ws.length - 1])) ws.pop();
|
|
11098
|
+
if (ws.length === 0) {
|
|
11099
|
+
if (idx > 0) { lines.pop(); idx--; ws = lines[idx].split(" "); continue; }
|
|
11100
|
+
ws = [first];
|
|
11101
|
+
break;
|
|
11102
|
+
}
|
|
11103
|
+
if (ws.length > 1 && ws.join(" ").length + 1 > W) { ws.pop(); continue; }
|
|
11104
|
+
break;
|
|
11085
11105
|
}
|
|
11086
|
-
lines
|
|
11106
|
+
lines.length = idx + 1;
|
|
11107
|
+
let kept = ws.join(" ").replace(/[\s,;:(\-\u2013\u2014]+$/, "");
|
|
11108
|
+
if (!kept) kept = ws[0];
|
|
11109
|
+
if (kept.length + 1 > W) kept = kept.slice(0, W - 1);
|
|
11110
|
+
lines[idx] = kept + "\u2026";
|
|
11087
11111
|
}
|
|
11088
11112
|
return lines;
|
|
11089
11113
|
};
|
|
@@ -11773,8 +11797,9 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
11773
11797
|
// Line 1 shows the head; the grey expansion CONTINUES from the same point (no dup, no
|
|
11774
11798
|
// split word).
|
|
11775
11799
|
const sp = fullLiveThought.lastIndexOf(" ", max - 1);
|
|
11776
|
-
|
|
11777
|
-
|
|
11800
|
+
// Any space at all beats a mid-word cut (#134); only a first word wider than the row is split.
|
|
11801
|
+
const cut = sp > 0 ? sp : max - 1;
|
|
11802
|
+
label = `${fullLiveThought.slice(0, cut).replace(/[\s,;:(\-\u2013\u2014]+$/, "")}… (${secs}s)`;
|
|
11778
11803
|
rest = fullLiveThought.slice(cut).trim();
|
|
11779
11804
|
} else {
|
|
11780
11805
|
const primary = runningTool
|
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.818",
|
|
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",
|