@tiens.nguyen/gonext-local-worker 1.0.243 → 1.0.246
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-repl.mjs +5 -4
- package/gonext_agent_chat.py +141 -20
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -1082,11 +1082,12 @@ async function runAgentTurn(history) {
|
|
|
1082
1082
|
const max = Math.max(24, (process.stdout.columns || 80) - 14);
|
|
1083
1083
|
const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
|
|
1084
1084
|
clearStatus();
|
|
1085
|
-
// Line 1: green ● +
|
|
1086
|
-
//
|
|
1087
|
-
// the
|
|
1085
|
+
// Line 1: green ● + green phase/seconds (matches the web's green streaming label).
|
|
1086
|
+
// Line 2 (indented): the SPINNER + the playful word together, both in the cycling
|
|
1087
|
+
// color — the spinner belongs with the word (dot on the label line, spinner leading
|
|
1088
|
+
// the flavor line).
|
|
1088
1089
|
process.stdout.write(
|
|
1089
|
-
`${green(BULLET)} ${
|
|
1090
|
+
`${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
|
|
1090
1091
|
"\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}`
|
|
1091
1092
|
);
|
|
1092
1093
|
statusShown = true;
|
package/gonext_agent_chat.py
CHANGED
|
@@ -3181,19 +3181,26 @@ def run_agent_chat(cfg):
|
|
|
3181
3181
|
# flakes, prove the test ran, cap retries) so it converges instead of chasing green.
|
|
3182
3182
|
_auto_test_block = (
|
|
3183
3183
|
"\nAUTO-TEST MODE (ON): after you CHANGE code you MUST verify it before "
|
|
3184
|
-
"final_answer — never finish on an untested change.
|
|
3185
|
-
"proves the change,
|
|
3186
|
-
" 1. grep_repo the edited file to confirm the change landed (
|
|
3187
|
-
" 2.
|
|
3188
|
-
"
|
|
3189
|
-
"
|
|
3184
|
+
"final_answer — never finish on an untested change. Climb the CHEAPEST check that "
|
|
3185
|
+
"proves the change, only as far as needed:\n"
|
|
3186
|
+
" 1. grep_repo the edited file to confirm the change landed (old code gone).\n"
|
|
3187
|
+
" 2. STATIC (cheapest — nothing runs): typecheck + lint. run_command('tsc --noEmit' "
|
|
3188
|
+
"/ 'mypy' / 'go vet'; 'eslint' / 'ruff'). Fix errors here first.\n"
|
|
3189
|
+
" 3. build / compile if the project does (npm run build / go build).\n"
|
|
3190
|
+
" 4. run the project's tests (npm test / pytest / …) if they exist — prefer the "
|
|
3191
|
+
"SINGLE test/file covering your change for speed (e.g. pytest path::test_x).\n"
|
|
3192
|
+
" 5. if it's a running app, start it and curl/fetch_url it, then check the response "
|
|
3190
3193
|
"contains what the change should produce.\n"
|
|
3191
|
-
"
|
|
3192
|
-
"
|
|
3193
|
-
"
|
|
3194
|
-
"
|
|
3195
|
-
"
|
|
3196
|
-
"
|
|
3194
|
+
"FIXING A BUG → write a MINIMAL test that reproduces it and run it FIRST: it must FAIL "
|
|
3195
|
+
"(red) BEFORE your fix and PASS (green) after. A test that passes before you change "
|
|
3196
|
+
"anything proves nothing. Keep it with the project's other tests.\n"
|
|
3197
|
+
"Before final_answer, run_command('git diff') (or 'git status' if not a git repo) to "
|
|
3198
|
+
"review EVERYTHING you changed — only what you intended, no stray edits or debug prints.\n"
|
|
3199
|
+
"DECIDE what 'passes' BEFORE you fix — do NOT weaken the check to make it green. On a "
|
|
3200
|
+
"FAIL: read the error, make ONE fix, RE-TEST the SAME way. Passes only after a no-change "
|
|
3201
|
+
"re-run = flaky, stop editing. Give up after 3 fix attempts on the same failure and "
|
|
3202
|
+
"final_answer honestly what still fails. Confirm a test actually exercised the change "
|
|
3203
|
+
"(the asserted behavior really ran), not an empty/no-op pass.\n"
|
|
3197
3204
|
if (_auto_test and _WS_AVAILABLE) else ""
|
|
3198
3205
|
)
|
|
3199
3206
|
# Deploy target (/server): when the user picked one, tell the agent to deploy THERE
|
|
@@ -3383,6 +3390,54 @@ def run_agent_chat(cfg):
|
|
|
3383
3390
|
# still deterministically deliver (e.g. render a partial PDF) instead of dead-ending.
|
|
3384
3391
|
_gathered: list = []
|
|
3385
3392
|
|
|
3393
|
+
# --- Loop / no-progress guards (task #79) -------------------------------------------
|
|
3394
|
+
# Weak coders thrash on a debug loop: rewrite a file to a previously-tried version,
|
|
3395
|
+
# re-hit a broken endpoint, and re-read unchanged files — burning the whole step
|
|
3396
|
+
# budget without new information. These caches let the tools detect and refuse that.
|
|
3397
|
+
_file_writes: dict = {} # realpath -> {content hashes already written this run}
|
|
3398
|
+
_url_5xx: dict = {} # localhost url -> how many 5xx it has returned
|
|
3399
|
+
_reads: dict = {} # realpath -> (mtime, identical-read count)
|
|
3400
|
+
|
|
3401
|
+
def _server_log_tail_for_url(url: str) -> str:
|
|
3402
|
+
"""If `url` is a localhost server we started via run_command, return the tail of
|
|
3403
|
+
its run-log — the REAL error. A 5xx returns only an HTML error page; the actual
|
|
3404
|
+
stack trace lives in the server's stdout log (task #79)."""
|
|
3405
|
+
try:
|
|
3406
|
+
import os as _os
|
|
3407
|
+
from urllib.parse import urlparse
|
|
3408
|
+
p = urlparse(url)
|
|
3409
|
+
if p.hostname not in ("localhost", "127.0.0.1", "0.0.0.0", "::1"):
|
|
3410
|
+
return ""
|
|
3411
|
+
for s in _ws_load_servers():
|
|
3412
|
+
if p.port and p.port in (s.get("ports") or []):
|
|
3413
|
+
logp = s.get("log") or ""
|
|
3414
|
+
if logp and _os.path.isfile(logp):
|
|
3415
|
+
with open(logp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3416
|
+
return _ws_distill_output(fh.read())[-1500:]
|
|
3417
|
+
return ""
|
|
3418
|
+
except Exception: # noqa: BLE001
|
|
3419
|
+
return ""
|
|
3420
|
+
|
|
3421
|
+
def _read_repeat_block(rp: str) -> str:
|
|
3422
|
+
"""Refuse a 3rd+ identical read of an UNCHANGED file (a common step-waster)."""
|
|
3423
|
+
try:
|
|
3424
|
+
import os as _os
|
|
3425
|
+
mt = _os.path.getmtime(rp)
|
|
3426
|
+
except OSError:
|
|
3427
|
+
return ""
|
|
3428
|
+
prev = _reads.get(rp)
|
|
3429
|
+
if prev and prev[0] == mt:
|
|
3430
|
+
cnt = prev[1] + 1
|
|
3431
|
+
_reads[rp] = (mt, cnt)
|
|
3432
|
+
if cnt >= 3:
|
|
3433
|
+
import os as _os2
|
|
3434
|
+
return (f"You've already read {_os2.path.basename(rp)} {cnt} times and it "
|
|
3435
|
+
"hasn't changed. Stop re-reading it — act on what it says, or "
|
|
3436
|
+
"call final_answer.")
|
|
3437
|
+
return ""
|
|
3438
|
+
_reads[rp] = (mt, 1)
|
|
3439
|
+
return ""
|
|
3440
|
+
|
|
3386
3441
|
def _dedup(tool_name: str, arg: str, next_hint: str):
|
|
3387
3442
|
"""Return (is_repeat, message). On a repeat, message steers the model forward."""
|
|
3388
3443
|
key = _norm_query(arg) if tool_name == "web_search" else (arg or "").strip().lower()
|
|
@@ -3452,6 +3507,23 @@ def run_agent_chat(cfg):
|
|
|
3452
3507
|
# Append a success tag to 2xx JSON responses so the model stops and calls final_answer.
|
|
3453
3508
|
elif result and result.startswith("HTTP 2"):
|
|
3454
3509
|
result = result + "\n[SUCCESS — call final_answer(response) now, do not parse or retry]"
|
|
3510
|
+
# A 5xx from a localhost dev server (task #79): the HTML error page hides the real
|
|
3511
|
+
# cause. Surface the SERVER'S OWN LOG (the actual stack trace) so the model fixes
|
|
3512
|
+
# the specific error instead of guessing/oscillating — and hard-stop after a few
|
|
3513
|
+
# repeats so it can't burn the whole budget re-hitting a broken endpoint.
|
|
3514
|
+
_m5 = re.match(r"HTTP\s+(5\d\d)", result or "")
|
|
3515
|
+
if _m5:
|
|
3516
|
+
_url_5xx[url] = _url_5xx.get(url, 0) + 1
|
|
3517
|
+
_tail = _server_log_tail_for_url(url)
|
|
3518
|
+
if _tail:
|
|
3519
|
+
result = (result + f"\n[SERVER ERROR LOG — the real cause of the "
|
|
3520
|
+
f"{_m5.group(1)} (fix THIS, don't guess):\n{_tail}\n]")
|
|
3521
|
+
if _url_5xx[url] >= 3:
|
|
3522
|
+
result = result + (
|
|
3523
|
+
f"\n[STOP: {url} has returned a 5xx {_url_5xx[url]} times. Do NOT request "
|
|
3524
|
+
"it again or rewrite the same file again. Fix the SPECIFIC error in the "
|
|
3525
|
+
"server log above, or call final_answer honestly stating what's failing.]"
|
|
3526
|
+
)
|
|
3455
3527
|
_emit({"type": "step", "text": f"HTTP {method.upper()} {url} → {status_line}"})
|
|
3456
3528
|
_log(f"http_request {method.upper()} {url} → {result[:80]}")
|
|
3457
3529
|
_last_obs["text"] = result
|
|
@@ -4307,14 +4379,23 @@ def run_agent_chat(cfg):
|
|
|
4307
4379
|
text = (_last_obs.get("text") or "").strip()
|
|
4308
4380
|
if text:
|
|
4309
4381
|
_log(f"max-steps fallback (last tool obs) → {text[:80]}")
|
|
4310
|
-
#
|
|
4311
|
-
#
|
|
4312
|
-
#
|
|
4313
|
-
#
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4382
|
+
# Summarize WHAT WAS TRIED + the current blocker, not just the last tool
|
|
4383
|
+
# observation (task #79): a bare last obs ('Stopped npm run dev') reads as a
|
|
4384
|
+
# non-answer and hides the loop the run got stuck in. Build a compact bullet
|
|
4385
|
+
# list of the tool calls from this turn's step trace + the last result.
|
|
4386
|
+
_tried = []
|
|
4387
|
+
for _st in _turn_steps[-8:]:
|
|
4388
|
+
_did = str(_st.get("did") or "").strip().replace("\n", " ")
|
|
4389
|
+
_m = re.search(r"([a-z_]+\([^\n]{0,70})", _did)
|
|
4390
|
+
_label = (_m.group(1) if _m else _did)[:70].strip()
|
|
4391
|
+
if _label:
|
|
4392
|
+
_tried.append(f" • {_label}")
|
|
4393
|
+
_summary = "I ran out of steps before finishing this."
|
|
4394
|
+
if _tried:
|
|
4395
|
+
_summary += " Here's what I tried:\n" + "\n".join(_tried)
|
|
4396
|
+
_summary += "\n\nCurrent blocker (last result):\n" + text[:600]
|
|
4397
|
+
_summary += "\n\nReply 'continue' to have me keep going from here."
|
|
4398
|
+
text = _summary
|
|
4318
4399
|
else:
|
|
4319
4400
|
# No usable tool output (e.g. every step's code failed to parse).
|
|
4320
4401
|
# Don't dead-end on a canned apology — answer from the conversation.
|
|
@@ -4425,6 +4506,10 @@ def run_agent_chat(cfg):
|
|
|
4425
4506
|
out = f"{root} is a file ({os.path.getsize(root)} bytes)."
|
|
4426
4507
|
_last_obs["text"] = out
|
|
4427
4508
|
return out
|
|
4509
|
+
_rb = _read_repeat_block(root)
|
|
4510
|
+
if _rb:
|
|
4511
|
+
_last_obs["text"] = _rb
|
|
4512
|
+
return _rb
|
|
4428
4513
|
entries = []
|
|
4429
4514
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
4430
4515
|
dirnames[:] = [d for d in dirnames if d not in _RAG_SKIP_DIRS]
|
|
@@ -4467,6 +4552,10 @@ def run_agent_chat(cfg):
|
|
|
4467
4552
|
msg = "Error: file too large to read (> 5 MB)."
|
|
4468
4553
|
_last_obs["text"] = msg
|
|
4469
4554
|
return msg
|
|
4555
|
+
_rb = _read_repeat_block(rp)
|
|
4556
|
+
if _rb:
|
|
4557
|
+
_last_obs["text"] = _rb
|
|
4558
|
+
return _rb
|
|
4470
4559
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
4471
4560
|
data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
|
|
4472
4561
|
out = _ws_cap_obs(data or "(empty file)")
|
|
@@ -4800,12 +4889,26 @@ def run_agent_chat(cfg):
|
|
|
4800
4889
|
try:
|
|
4801
4890
|
rp = _ws_write_allowed((path or "").strip())
|
|
4802
4891
|
import os
|
|
4892
|
+
import hashlib as _hl
|
|
4803
4893
|
existed = os.path.exists(rp)
|
|
4804
4894
|
if existed and not overwrite:
|
|
4805
4895
|
msg = (f"Error: {path} already exists — pass overwrite=True to replace the "
|
|
4806
4896
|
"whole file, or use edit_lines/edit_file for a partial change.")
|
|
4807
4897
|
_last_obs["text"] = msg
|
|
4808
4898
|
return msg
|
|
4899
|
+
# Oscillation guard (task #79): if the model rewrites a file to a version it
|
|
4900
|
+
# ALREADY wrote this run (the NextResponse ↔ new Response flip-flop), that
|
|
4901
|
+
# exact content already failed — refuse instead of looping on a non-fix.
|
|
4902
|
+
_ch = _hl.sha1((content or "").encode("utf-8", "replace")).hexdigest()
|
|
4903
|
+
_seen = _file_writes.setdefault(rp, set())
|
|
4904
|
+
if existed and _ch in _seen:
|
|
4905
|
+
msg = (f"You already wrote this EXACT content to {os.path.basename(rp)} "
|
|
4906
|
+
"earlier this task and it did not fix the problem — writing it "
|
|
4907
|
+
"again won't help. Do NOT rewrite this file with the same content. "
|
|
4908
|
+
"Read the server error log, try a genuinely DIFFERENT fix, or call "
|
|
4909
|
+
"final_answer honestly stating what's still failing.")
|
|
4910
|
+
_last_obs["text"] = msg
|
|
4911
|
+
return msg
|
|
4809
4912
|
# Snapshot BEFORE overwriting so revert restores the prior version. Record as
|
|
4810
4913
|
# an "edit" (not "create") when the file already existed, so revert restores
|
|
4811
4914
|
# the backup instead of deleting a file the user may have had before.
|
|
@@ -4820,6 +4923,7 @@ def run_agent_chat(cfg):
|
|
|
4820
4923
|
("Rewrote file — " if existed else "Added ") + f"{len(content_ls)} line(s)",
|
|
4821
4924
|
added=[(i + 1, t) for i, t in enumerate(content_ls)],
|
|
4822
4925
|
)})
|
|
4926
|
+
_seen.add(_ch) # remember this content so a later revert-to-it is caught
|
|
4823
4927
|
out = f"{'Overwrote' if existed else 'Created'} {rp} ({len(content)} chars)."
|
|
4824
4928
|
_last_obs["text"] = out
|
|
4825
4929
|
return out
|
|
@@ -4938,6 +5042,23 @@ def run_agent_chat(cfg):
|
|
|
4938
5042
|
_last_obs["text"] = msg
|
|
4939
5043
|
return msg
|
|
4940
5044
|
# else: destructive but non-interactive → allowed by default (unchanged)
|
|
5045
|
+
# Don't spawn a DUPLICATE of a server that's already running (task #79): the
|
|
5046
|
+
# model re-runs 'npm run dev' and Next/Vite then bind a SECOND port, which
|
|
5047
|
+
# confused the whole debug loop (3000 500 vs 3001 refused). If the identical
|
|
5048
|
+
# command+workdir is still listening, reuse it.
|
|
5049
|
+
for _srv in _ws_load_servers():
|
|
5050
|
+
if _srv.get("command") == command and _srv.get("workdir") == wd:
|
|
5051
|
+
_live = _ws_listening_ports(_srv.get("pid") or 0)
|
|
5052
|
+
if _live:
|
|
5053
|
+
_urls = ", ".join(f"http://localhost:{p}" for p in _live)
|
|
5054
|
+
msg = (f"'{command}' is ALREADY running — {_urls} (pid "
|
|
5055
|
+
f"{_srv.get('pid')}). Reusing it; do NOT start another. A "
|
|
5056
|
+
"dev server hot-reloads code changes automatically, so just "
|
|
5057
|
+
f"request {_urls} again. Use stop_server first only if you "
|
|
5058
|
+
"truly need a full restart.")
|
|
5059
|
+
_emit({"type": "step", "text": f"Server already up → {_urls} (reused)"})
|
|
5060
|
+
_last_obs["text"] = msg
|
|
5061
|
+
return msg
|
|
4941
5062
|
_emit({"type": "step", "text": f"Running → {command[:70]}"})
|
|
4942
5063
|
t = max(5, min(int(timeout_seconds or 180), 600))
|
|
4943
5064
|
# Own process group (start_new_session) + output to a log file. This is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.246",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|