@tiens.nguyen/gu-cli 1.0.703 → 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.
- package/agent-model-command.mjs +34 -0
- package/client-expert-api.mjs +10 -7
- package/gonext_agent_chat.py +17 -6
- package/gu-repl.mjs +83 -9
- package/package.json +2 -1
- package/verbose-lines.mjs +33 -0
package/agent-model-command.mjs
CHANGED
|
@@ -466,3 +466,37 @@ export function agentProviderChoices(settings) {
|
|
|
466
466
|
out.push({ action: "add", label: "add a new OpenAI-compatible provider…" });
|
|
467
467
|
return out;
|
|
468
468
|
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------------------------
|
|
471
|
+
// The Ollama CLOUD's key is its OWN — and a key is never a key's name.
|
|
472
|
+
// ---------------------------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The key saved FOR this provider row, or null.
|
|
476
|
+
*
|
|
477
|
+
* Reported: /agent-model → Ollama → https://ollama.com listed "20 on https://ollama.com" and saved the
|
|
478
|
+
* agent without asking for a key; the coder on ollama.com then failed "REJECTED the coding API key".
|
|
479
|
+
* Two things hid it. ollama.com's /api/tags is PUBLIC (200 with no key), so a successful list proves
|
|
480
|
+
* nothing about a key. And "a key for this provider" counted UNBOUND keys — by design usable with any
|
|
481
|
+
* OpenAI-compatible provider — so the account's sk-… key was taken as ollama.com's, and refused there.
|
|
482
|
+
* For the Ollama cloud only a key BOUND to its row counts.
|
|
483
|
+
*/
|
|
484
|
+
export function boundKeyFor(keys, rowId) {
|
|
485
|
+
const id = String(rowId ?? "").trim();
|
|
486
|
+
if (!id || !Array.isArray(keys)) return null;
|
|
487
|
+
return keys.find((k) => k && k.backendId === id) ?? null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Does what was typed as a key's NAME look like the key itself?
|
|
492
|
+
*
|
|
493
|
+
* Reported in the same log: a key named "sk-E8Iv…3E66" — the secret pasted into the name prompt — is
|
|
494
|
+
* printed in full wherever keys are listed ("API key sk-E8Iv… ····3E66"). Names are shown; secrets
|
|
495
|
+
* must not be. Caught on the known prefixes, and on anything long, unbroken and mixed like a token.
|
|
496
|
+
*/
|
|
497
|
+
export function looksLikeSecret(name) {
|
|
498
|
+
const v = String(name ?? "").trim();
|
|
499
|
+
if (!v) return false;
|
|
500
|
+
if (/^(sk-|sk_|pk-|rk-|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|gsk_|ollama_)/i.test(v)) return true;
|
|
501
|
+
return v.length >= 24 && !/\s/.test(v) && /[A-Za-z]/.test(v) && /\d/.test(v);
|
|
502
|
+
}
|
package/client-expert-api.mjs
CHANGED
|
@@ -753,14 +753,17 @@ export function mergeOllamaServerBackends(existing, fresh, { isCloud } = {}) {
|
|
|
753
753
|
const cloud = typeof isCloud === "function" ? isCloud : () => false;
|
|
754
754
|
const norm = (u) => String(u ?? "").trim().replace(/\/+$/, "").toLowerCase();
|
|
755
755
|
const rows = Array.isArray(existing) ? existing.filter(Boolean) : [];
|
|
756
|
-
const
|
|
757
|
-
const
|
|
756
|
+
const freshOllama = (Array.isArray(fresh) ? fresh : []).filter((f) => f.kind === "ollama");
|
|
757
|
+
const newUrls = new Set(freshOllama.map((f) => norm(f.url)));
|
|
758
|
+
// A row already at the NEW server's URL is the same provider — the new server takes it over, id and
|
|
759
|
+
// all, so a key bound to it (the Ollama cloud's own key) keeps working and no twin row appears.
|
|
760
|
+
const sameServer = rows.filter((b) => newUrls.has(norm(b.url)));
|
|
761
|
+
const replaced = [...sameServer, ...rows.filter((b) => b.kind === "ollama" && !cloud(b.url) && !sameServer.includes(b))];
|
|
762
|
+
const kept = rows.filter((b) => !replaced.includes(b));
|
|
758
763
|
const out = [];
|
|
759
|
-
for (const f of
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
out.push(id ? { ...f, id } : f);
|
|
763
|
-
}
|
|
764
|
+
for (const f of freshOllama) {
|
|
765
|
+
const id = (sameServer.find((b) => b.id && norm(b.url) === norm(f.url)) ?? replaced.find((b) => b.id))?.id;
|
|
766
|
+
out.push(id ? { ...f, id } : f);
|
|
764
767
|
}
|
|
765
768
|
out.push(...kept);
|
|
766
769
|
for (const f of Array.isArray(fresh) ? fresh : []) {
|
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -112,7 +112,7 @@ import {
|
|
|
112
112
|
shouldCancelPrompt,
|
|
113
113
|
} from "./terminal-layout.mjs";
|
|
114
114
|
import { completedFlush } from "./completed-flush.mjs";
|
|
115
|
-
import { codingBannerLabel, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
115
|
+
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
116
116
|
import { codingChoiceTags, coderDisplayName, ollamaWhere } from "./coding-choice-tags.mjs";
|
|
117
117
|
import {
|
|
118
118
|
isDoubleClick,
|
|
@@ -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,
|
|
@@ -4955,6 +4956,28 @@ async function reconfigureOllamaServer() {
|
|
|
4955
4956
|
console.log(GUTTER + dim("if this machine's local API is old, restart it with `gu api restart`.\n"));
|
|
4956
4957
|
return;
|
|
4957
4958
|
}
|
|
4959
|
+
// THE OLLAMA CLOUD NEEDS ITS OWN KEY. Reported: choosing https://ollama.com here listed its 20 models
|
|
4960
|
+
// (that list is public) and saved, without a key — and the next turn failed "REJECTED the coding API
|
|
4961
|
+
// key", having borrowed an unrelated sk-… key. Asked for now, bound to the row just saved.
|
|
4962
|
+
{
|
|
4963
|
+
const cloudUrls = [coder?.normalized?.url, agent?.normalized?.url].filter(
|
|
4964
|
+
(u, i, all) => u && all.indexOf(u) === i && isOllamaCloudUrl(u)
|
|
4965
|
+
);
|
|
4966
|
+
if (cloudUrls.length) {
|
|
4967
|
+
const fresh = await withPendingStatus("checking the Ollama cloud key", fetchWorkerSettings);
|
|
4968
|
+
const keys = await fetchCodingKeys();
|
|
4969
|
+
const ps = await import("./coding-provider-setup.mjs");
|
|
4970
|
+
for (const u of cloudUrls) {
|
|
4971
|
+
const row = ps
|
|
4972
|
+
.existingBackendRows(fresh ?? {})
|
|
4973
|
+
.find((b) => ps.ensureV1Root(String(b.url ?? "")).toLowerCase() === ps.ensureV1Root(u).toLowerCase());
|
|
4974
|
+
if (row && !am.boundKeyFor(keys, row.id)) {
|
|
4975
|
+
console.log(yellow(` ${hostOf(u)} is the Ollama cloud — it needs its own API key (the model list worked without one).`));
|
|
4976
|
+
await askKeyForRow(row, hostOf(u));
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4958
4981
|
// THE ACCOUNT DEFAULT JUST CHANGED — say so in memory, or the bar reports the old one.
|
|
4959
4982
|
//
|
|
4960
4983
|
// The override is cleared just below, correctly: what was picked here IS the account default
|
|
@@ -5514,7 +5537,7 @@ async function addCodingProvider(roleName = "coding") {
|
|
|
5514
5537
|
// NOT DEFAULTED, by request. An empty name becomes "Unknown" at the API, which is the same
|
|
5515
5538
|
// thing every other add-a-key path produces — the URL default was asked for, this was not.
|
|
5516
5539
|
console.log("\n" + GUTTER + dim('A name just tells keys apart — "work", "personal".'));
|
|
5517
|
-
const name =
|
|
5540
|
+
const name = await askKeyName();
|
|
5518
5541
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
5519
5542
|
const secret = (await askSecret(` API key${name ? ` for ${name}` : ""}: `)).trim();
|
|
5520
5543
|
if (!secret) {
|
|
@@ -6048,10 +6071,25 @@ async function saveAgentModel(agentUrl, model, host) {
|
|
|
6048
6071
|
return true;
|
|
6049
6072
|
}
|
|
6050
6073
|
|
|
6074
|
+
/**
|
|
6075
|
+
* Ask for a key's NAME — and refuse the key itself as a name: names are printed wherever keys are
|
|
6076
|
+
* listed, so a pasted secret there is a secret on screen (reported: "API key sk-E8Iv… ····3E66").
|
|
6077
|
+
*/
|
|
6078
|
+
async function askKeyName(prompt = " key name: ") {
|
|
6079
|
+
const am = await import("./agent-model-command.mjs");
|
|
6080
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
6081
|
+
const name = (await ask(prompt)).trim();
|
|
6082
|
+
if (!am.looksLikeSecret(name)) return name;
|
|
6083
|
+
console.log(yellow(" that looks like the API key itself — the NAME is shown on screen, so it must not be the key."));
|
|
6084
|
+
console.log(GUTTER + dim('type a short label like "work", or press Enter for none — the key comes next.'));
|
|
6085
|
+
}
|
|
6086
|
+
return "";
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6051
6089
|
/** Ask for a key and store it BOUND to this provider row. Returns the secret, or "" if cancelled. */
|
|
6052
6090
|
async function askKeyForRow(row, host) {
|
|
6053
6091
|
console.log("\n" + GUTTER + dim('A name just tells keys apart — "work", "personal".'));
|
|
6054
|
-
const name =
|
|
6092
|
+
const name = await askKeyName();
|
|
6055
6093
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
6056
6094
|
const secret = (await askSecret(` API key for ${host}${name ? ` (${name})` : ""}: `)).trim();
|
|
6057
6095
|
if (!secret) {
|
|
@@ -6186,9 +6224,15 @@ async function addOllamaAgent() {
|
|
|
6186
6224
|
: null;
|
|
6187
6225
|
let key = "";
|
|
6188
6226
|
let newRow = null;
|
|
6227
|
+
let hasBoundKey = false;
|
|
6189
6228
|
if (target.cloud) {
|
|
6190
|
-
|
|
6191
|
-
|
|
6229
|
+
// A KEY SAVED FOR THIS PROVIDER — not any key the account holds. ollama.com's model list answers
|
|
6230
|
+
// without a key, so listing cannot catch a borrowed one; only asking can.
|
|
6231
|
+
if (row) {
|
|
6232
|
+
const keys = await withPendingStatus(`reading the keys for ${target.host}`, fetchCodingKeys);
|
|
6233
|
+
hasBoundKey = Boolean(am.boundKeyFor(keys, row.id));
|
|
6234
|
+
}
|
|
6235
|
+
if (!hasBoundKey) {
|
|
6192
6236
|
if (!row) {
|
|
6193
6237
|
if (existing.length >= ps.MAX_BACKENDS) {
|
|
6194
6238
|
console.log(yellow(` ${ps.MAX_BACKENDS} providers is the limit — remove one in web Settings → Agent first.\n`));
|
|
@@ -6251,7 +6295,7 @@ async function addOllamaAgent() {
|
|
|
6251
6295
|
row = newRow;
|
|
6252
6296
|
console.log(GUTTER + dim(`${target.host} is also offered in /model now.`));
|
|
6253
6297
|
}
|
|
6254
|
-
if (target.cloud && key && !
|
|
6298
|
+
if (target.cloud && key && !hasBoundKey) {
|
|
6255
6299
|
const id = await withPendingStatus("saving the key", () => addCodingKey("Ollama cloud", key, String(row?.id ?? "")));
|
|
6256
6300
|
if (!id) {
|
|
6257
6301
|
console.log(red(" couldn't save the key — the agent was not changed.\n"));
|
|
@@ -7191,7 +7235,7 @@ async function deleteCodingKey(id) {
|
|
|
7191
7235
|
*/
|
|
7192
7236
|
async function promptNewCodingKey(backendId = "", provider = "") {
|
|
7193
7237
|
console.log("\n" + GUTTER + dim("A name just tells them apart — \"work\", \"personal\".\n"));
|
|
7194
|
-
const name =
|
|
7238
|
+
const name = await askKeyName(" Name for this key: ");
|
|
7195
7239
|
console.log(GUTTER + dim(SECRET_KEY_HINT));
|
|
7196
7240
|
const secret = (await askSecret(` API key${name ? ` for ${name}` : ""}: `)).trim();
|
|
7197
7241
|
if (!secret) {
|
|
@@ -8423,6 +8467,16 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8423
8467
|
// 400 chars — enough for a one-line label, not for the paragraphs we now print). Bounded so a
|
|
8424
8468
|
// runaway generation cannot grow without limit; the prose we keep is far shorter anyway.
|
|
8425
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
|
+
};
|
|
8426
8480
|
let hoverRow = -1; // last mouse viewport row (from motion events) → highlight when it's on the thought line
|
|
8427
8481
|
let thoughtRow = -1; // absolute viewport row of the thought line (line 1), learned via \x1b[6n
|
|
8428
8482
|
let lastDsrAt = 0; // throttle the cursor-position probe
|
|
@@ -8851,7 +8905,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8851
8905
|
carry += stripThinkTags(chunk);
|
|
8852
8906
|
let nl;
|
|
8853
8907
|
while ((nl = carry.indexOf("\n")) >= 0) {
|
|
8854
|
-
|
|
8908
|
+
let line = carry.slice(0, nl);
|
|
8855
8909
|
carry = carry.slice(nl + 1);
|
|
8856
8910
|
const alreadyFlushed = carryFlushedLen;
|
|
8857
8911
|
carryFlushedLen = 0; // reset — `carry` now starts a fresh pending line
|
|
@@ -8864,6 +8918,21 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8864
8918
|
almostDone = /almost/i.test(line);
|
|
8865
8919
|
continue;
|
|
8866
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();
|
|
8867
8936
|
// Task #136: a RAG index file was written. Swallow the line — it belongs in the
|
|
8868
8937
|
// BOTTOM BAR, not in the action list — and remember the newest one. Kept out of the
|
|
8869
8938
|
// bullet stream deliberately: indexing writes a shard per batch, so printing them
|
|
@@ -8896,7 +8965,11 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
8896
8965
|
// The step's model output is complete. Print its reasoning HERE, before the tools
|
|
8897
8966
|
// report what they did, so the transcript reads in the order it happened: the model
|
|
8898
8967
|
// says what it intends, then the actions it took appear under it.
|
|
8899
|
-
if (
|
|
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
|
+
}
|
|
8900
8973
|
stepThought = "";
|
|
8901
8974
|
}
|
|
8902
8975
|
continue;
|
|
@@ -9285,6 +9358,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
9285
9358
|
lastWasBlank = false;
|
|
9286
9359
|
}
|
|
9287
9360
|
}
|
|
9361
|
+
releaseHeldThought(); // #45: nothing followed it, so it was not a false start
|
|
9288
9362
|
clearStatus();
|
|
9289
9363
|
// Task #104: expose this turn's code-model INPUT + the CUMULATIVE output-token
|
|
9290
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.
|
|
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
|
+
}
|