@tiens.nguyen/gonext-local-worker 1.0.244 → 1.0.247
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-local-worker.mjs +3 -0
- package/gonext-repl.mjs +34 -3
- package/gonext_agent_chat.py +175 -21
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1879,6 +1879,9 @@ async function runAgentChatJob(job) {
|
|
|
1879
1879
|
// REPL's "I can show a picker" signal (else python keeps hard-blocking, as on web).
|
|
1880
1880
|
jobId,
|
|
1881
1881
|
interactiveApproval: payload?.interactiveApproval === true,
|
|
1882
|
+
// Session opt-in (task #80): auto-extend past the step budget without re-prompting
|
|
1883
|
+
// (the REPL sets this once the user picks "yes, don't ask again").
|
|
1884
|
+
alwaysExtendOnMaxStep: payload?.alwaysExtendOnMaxStep === true,
|
|
1882
1885
|
// Auto-test mode (/test-auto): the agent verifies its work and fixes until it passes.
|
|
1883
1886
|
autoTest: payload?.autoTest === true,
|
|
1884
1887
|
// Deploy target chosen via the terminal /server picker (task #69). Host/user only.
|
package/gonext-repl.mjs
CHANGED
|
@@ -778,8 +778,25 @@ function onApprovalKey(key) {
|
|
|
778
778
|
// Show the picker and resolve to the user's choice. Non-TTY (piped) can't pick → deny
|
|
779
779
|
// (safe). Auto-denies after ~170s so an absent user never hangs the turn (and stays
|
|
780
780
|
// under the python side's 180s wait); Enter is instant since Yes is preselected.
|
|
781
|
-
|
|
782
|
-
|
|
781
|
+
// The command carries a "__MAXSTEP__::<message>" sentinel when it's the step-budget
|
|
782
|
+
// continue prompt (task #80) rather than a risky-command approval. That case shows a
|
|
783
|
+
// 3-option picker (Yes / Yes-don't-ask-again / No); a normal command shows Yes/No.
|
|
784
|
+
// Returns { allow, dontAsk } — dontAsk is only ever true for the max-step "don't ask
|
|
785
|
+
// again this session" choice (the caller then stops sending the prompt).
|
|
786
|
+
const MAXSTEP_PREFIX = "__MAXSTEP__::";
|
|
787
|
+
async function approvalPrompt(command) {
|
|
788
|
+
const isMax = command.startsWith(MAXSTEP_PREFIX);
|
|
789
|
+
if (isMax) {
|
|
790
|
+
const label = command.slice(MAXSTEP_PREFIX.length);
|
|
791
|
+
if (!process.stdin.isTTY) return { allow: false, dontAsk: false };
|
|
792
|
+
process.stdout.write("\n" + yellow("⏭ " + label) + "\n");
|
|
793
|
+
const idx = await pickFromList(
|
|
794
|
+
["Yes, keep going", "Yes, and don't ask again this session", "No, stop here"],
|
|
795
|
+
0
|
|
796
|
+
);
|
|
797
|
+
return { allow: idx === 0 || idx === 1, dontAsk: idx === 1 };
|
|
798
|
+
}
|
|
799
|
+
const allow = await new Promise((resolve) => {
|
|
783
800
|
if (!process.stdin.isTTY) {
|
|
784
801
|
process.stdout.write(dim(`\n(approval needed for: ${command} — no interactive terminal, skipping)\n`));
|
|
785
802
|
resolve(false);
|
|
@@ -799,6 +816,7 @@ function approvalPrompt(command) {
|
|
|
799
816
|
orig(v);
|
|
800
817
|
};
|
|
801
818
|
});
|
|
819
|
+
return { allow, dontAsk: false };
|
|
802
820
|
}
|
|
803
821
|
// Generic arrow-key list picker (↑/↓ + Enter) — used by /server so you navigate instead
|
|
804
822
|
// of typing a number. Same mechanism as the Yes/No approval picker above, generalized to
|
|
@@ -860,6 +878,10 @@ let sessionCodingModel = "";
|
|
|
860
878
|
// and fixes → re-tests until it passes before finishing. Default off; remembered per
|
|
861
879
|
// folder in the session file; sent to /agent-ask as autoTest so python drives the loop.
|
|
862
880
|
let sessionTestAuto = false;
|
|
881
|
+
// Step-budget opt-out (task #80): set once the user picks "yes, don't ask again" at a
|
|
882
|
+
// max-step prompt. Session-scoped (not persisted per folder); sent to /agent-ask each turn
|
|
883
|
+
// so the agent auto-extends past the step budget without prompting again.
|
|
884
|
+
let alwaysExtendOnMaxStep = false;
|
|
863
885
|
// Deploy target chosen via /server (task #69): {name, host, user} or null. Session-scoped;
|
|
864
886
|
// sent to /agent-ask as deployServer so the agent deploys to this host with KEY auth.
|
|
865
887
|
let selectedServer = null;
|
|
@@ -965,6 +987,9 @@ async function runAgentTurn(history) {
|
|
|
965
987
|
// The terminal can show a Yes/No picker → the agent PAUSES on a risky command and
|
|
966
988
|
// asks instead of hard-blocking it (interactive command-approval gate).
|
|
967
989
|
interactiveApproval: true,
|
|
990
|
+
// Step-budget opt-out (task #80): once the user chose "don't ask again", auto-extend
|
|
991
|
+
// past the step budget without prompting.
|
|
992
|
+
...(alwaysExtendOnMaxStep ? { alwaysExtendOnMaxStep: true } : {}),
|
|
968
993
|
// Auto-test mode (/test-auto): the agent verifies its work and fixes until it passes.
|
|
969
994
|
...(sessionTestAuto ? { autoTest: true } : {}),
|
|
970
995
|
// Deploy target chosen via /server — host/user only (no secret), for key-auth deploys.
|
|
@@ -1409,7 +1434,13 @@ async function runAgentTurn(history) {
|
|
|
1409
1434
|
) {
|
|
1410
1435
|
lastApprovalId = job.pendingApproval.id;
|
|
1411
1436
|
clearStatus();
|
|
1412
|
-
const allow = await approvalPrompt(job.pendingApproval.command || "");
|
|
1437
|
+
const { allow, dontAsk } = await approvalPrompt(job.pendingApproval.command || "");
|
|
1438
|
+
// "Yes, don't ask again this session" (max-step prompt only): remember it so every
|
|
1439
|
+
// later turn sends alwaysExtendOnMaxStep and the agent auto-extends silently (#80).
|
|
1440
|
+
if (dontAsk) {
|
|
1441
|
+
alwaysExtendOnMaxStep = true;
|
|
1442
|
+
process.stdout.write(dim(" (won't ask again about the step budget this session)\n"));
|
|
1443
|
+
}
|
|
1413
1444
|
try {
|
|
1414
1445
|
await fetch(`${apiBase}/api/worker/jobs/${jobId}/approval`, {
|
|
1415
1446
|
method: "POST",
|
package/gonext_agent_chat.py
CHANGED
|
@@ -2340,7 +2340,10 @@ def _ws_request_approval(api_base, worker_key, job_id, command, reason):
|
|
|
2340
2340
|
except Exception as e: # noqa: BLE001
|
|
2341
2341
|
_log(f"approval-request failed: {e} — treating as deny")
|
|
2342
2342
|
return False
|
|
2343
|
-
|
|
2343
|
+
if command.startswith("__MAXSTEP__::"):
|
|
2344
|
+
_emit({"type": "step", "text": "Awaiting your choice: continue past the step budget?"})
|
|
2345
|
+
else:
|
|
2346
|
+
_emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
|
|
2344
2347
|
deadline = _t.time() + 180 # 3 min; absent user → deny (never auto-run something risky)
|
|
2345
2348
|
while _t.time() < deadline:
|
|
2346
2349
|
_t.sleep(1.2)
|
|
@@ -3181,19 +3184,26 @@ def run_agent_chat(cfg):
|
|
|
3181
3184
|
# flakes, prove the test ran, cap retries) so it converges instead of chasing green.
|
|
3182
3185
|
_auto_test_block = (
|
|
3183
3186
|
"\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
|
-
"
|
|
3187
|
+
"final_answer — never finish on an untested change. Climb the CHEAPEST check that "
|
|
3188
|
+
"proves the change, only as far as needed:\n"
|
|
3189
|
+
" 1. grep_repo the edited file to confirm the change landed (old code gone).\n"
|
|
3190
|
+
" 2. STATIC (cheapest — nothing runs): typecheck + lint. run_command('tsc --noEmit' "
|
|
3191
|
+
"/ 'mypy' / 'go vet'; 'eslint' / 'ruff'). Fix errors here first.\n"
|
|
3192
|
+
" 3. build / compile if the project does (npm run build / go build).\n"
|
|
3193
|
+
" 4. run the project's tests (npm test / pytest / …) if they exist — prefer the "
|
|
3194
|
+
"SINGLE test/file covering your change for speed (e.g. pytest path::test_x).\n"
|
|
3195
|
+
" 5. if it's a running app, start it and curl/fetch_url it, then check the response "
|
|
3190
3196
|
"contains what the change should produce.\n"
|
|
3191
|
-
"
|
|
3192
|
-
"
|
|
3193
|
-
"
|
|
3194
|
-
"
|
|
3195
|
-
"
|
|
3196
|
-
"
|
|
3197
|
+
"FIXING A BUG → write a MINIMAL test that reproduces it and run it FIRST: it must FAIL "
|
|
3198
|
+
"(red) BEFORE your fix and PASS (green) after. A test that passes before you change "
|
|
3199
|
+
"anything proves nothing. Keep it with the project's other tests.\n"
|
|
3200
|
+
"Before final_answer, run_command('git diff') (or 'git status' if not a git repo) to "
|
|
3201
|
+
"review EVERYTHING you changed — only what you intended, no stray edits or debug prints.\n"
|
|
3202
|
+
"DECIDE what 'passes' BEFORE you fix — do NOT weaken the check to make it green. On a "
|
|
3203
|
+
"FAIL: read the error, make ONE fix, RE-TEST the SAME way. Passes only after a no-change "
|
|
3204
|
+
"re-run = flaky, stop editing. Give up after 3 fix attempts on the same failure and "
|
|
3205
|
+
"final_answer honestly what still fails. Confirm a test actually exercised the change "
|
|
3206
|
+
"(the asserted behavior really ran), not an empty/no-op pass.\n"
|
|
3197
3207
|
if (_auto_test and _WS_AVAILABLE) else ""
|
|
3198
3208
|
)
|
|
3199
3209
|
# Deploy target (/server): when the user picked one, tell the agent to deploy THERE
|
|
@@ -3383,6 +3393,54 @@ def run_agent_chat(cfg):
|
|
|
3383
3393
|
# still deterministically deliver (e.g. render a partial PDF) instead of dead-ending.
|
|
3384
3394
|
_gathered: list = []
|
|
3385
3395
|
|
|
3396
|
+
# --- Loop / no-progress guards (task #79) -------------------------------------------
|
|
3397
|
+
# Weak coders thrash on a debug loop: rewrite a file to a previously-tried version,
|
|
3398
|
+
# re-hit a broken endpoint, and re-read unchanged files — burning the whole step
|
|
3399
|
+
# budget without new information. These caches let the tools detect and refuse that.
|
|
3400
|
+
_file_writes: dict = {} # realpath -> {content hashes already written this run}
|
|
3401
|
+
_url_5xx: dict = {} # localhost url -> how many 5xx it has returned
|
|
3402
|
+
_reads: dict = {} # realpath -> (mtime, identical-read count)
|
|
3403
|
+
|
|
3404
|
+
def _server_log_tail_for_url(url: str) -> str:
|
|
3405
|
+
"""If `url` is a localhost server we started via run_command, return the tail of
|
|
3406
|
+
its run-log — the REAL error. A 5xx returns only an HTML error page; the actual
|
|
3407
|
+
stack trace lives in the server's stdout log (task #79)."""
|
|
3408
|
+
try:
|
|
3409
|
+
import os as _os
|
|
3410
|
+
from urllib.parse import urlparse
|
|
3411
|
+
p = urlparse(url)
|
|
3412
|
+
if p.hostname not in ("localhost", "127.0.0.1", "0.0.0.0", "::1"):
|
|
3413
|
+
return ""
|
|
3414
|
+
for s in _ws_load_servers():
|
|
3415
|
+
if p.port and p.port in (s.get("ports") or []):
|
|
3416
|
+
logp = s.get("log") or ""
|
|
3417
|
+
if logp and _os.path.isfile(logp):
|
|
3418
|
+
with open(logp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3419
|
+
return _ws_distill_output(fh.read())[-1500:]
|
|
3420
|
+
return ""
|
|
3421
|
+
except Exception: # noqa: BLE001
|
|
3422
|
+
return ""
|
|
3423
|
+
|
|
3424
|
+
def _read_repeat_block(rp: str) -> str:
|
|
3425
|
+
"""Refuse a 3rd+ identical read of an UNCHANGED file (a common step-waster)."""
|
|
3426
|
+
try:
|
|
3427
|
+
import os as _os
|
|
3428
|
+
mt = _os.path.getmtime(rp)
|
|
3429
|
+
except OSError:
|
|
3430
|
+
return ""
|
|
3431
|
+
prev = _reads.get(rp)
|
|
3432
|
+
if prev and prev[0] == mt:
|
|
3433
|
+
cnt = prev[1] + 1
|
|
3434
|
+
_reads[rp] = (mt, cnt)
|
|
3435
|
+
if cnt >= 3:
|
|
3436
|
+
import os as _os2
|
|
3437
|
+
return (f"You've already read {_os2.path.basename(rp)} {cnt} times and it "
|
|
3438
|
+
"hasn't changed. Stop re-reading it — act on what it says, or "
|
|
3439
|
+
"call final_answer.")
|
|
3440
|
+
return ""
|
|
3441
|
+
_reads[rp] = (mt, 1)
|
|
3442
|
+
return ""
|
|
3443
|
+
|
|
3386
3444
|
def _dedup(tool_name: str, arg: str, next_hint: str):
|
|
3387
3445
|
"""Return (is_repeat, message). On a repeat, message steers the model forward."""
|
|
3388
3446
|
key = _norm_query(arg) if tool_name == "web_search" else (arg or "").strip().lower()
|
|
@@ -3452,6 +3510,23 @@ def run_agent_chat(cfg):
|
|
|
3452
3510
|
# Append a success tag to 2xx JSON responses so the model stops and calls final_answer.
|
|
3453
3511
|
elif result and result.startswith("HTTP 2"):
|
|
3454
3512
|
result = result + "\n[SUCCESS — call final_answer(response) now, do not parse or retry]"
|
|
3513
|
+
# A 5xx from a localhost dev server (task #79): the HTML error page hides the real
|
|
3514
|
+
# cause. Surface the SERVER'S OWN LOG (the actual stack trace) so the model fixes
|
|
3515
|
+
# the specific error instead of guessing/oscillating — and hard-stop after a few
|
|
3516
|
+
# repeats so it can't burn the whole budget re-hitting a broken endpoint.
|
|
3517
|
+
_m5 = re.match(r"HTTP\s+(5\d\d)", result or "")
|
|
3518
|
+
if _m5:
|
|
3519
|
+
_url_5xx[url] = _url_5xx.get(url, 0) + 1
|
|
3520
|
+
_tail = _server_log_tail_for_url(url)
|
|
3521
|
+
if _tail:
|
|
3522
|
+
result = (result + f"\n[SERVER ERROR LOG — the real cause of the "
|
|
3523
|
+
f"{_m5.group(1)} (fix THIS, don't guess):\n{_tail}\n]")
|
|
3524
|
+
if _url_5xx[url] >= 3:
|
|
3525
|
+
result = result + (
|
|
3526
|
+
f"\n[STOP: {url} has returned a 5xx {_url_5xx[url]} times. Do NOT request "
|
|
3527
|
+
"it again or rewrite the same file again. Fix the SPECIFIC error in the "
|
|
3528
|
+
"server log above, or call final_answer honestly stating what's failing.]"
|
|
3529
|
+
)
|
|
3455
3530
|
_emit({"type": "step", "text": f"HTTP {method.upper()} {url} → {status_line}"})
|
|
3456
3531
|
_log(f"http_request {method.upper()} {url} → {result[:80]}")
|
|
3457
3532
|
_last_obs["text"] = result
|
|
@@ -4307,14 +4382,23 @@ def run_agent_chat(cfg):
|
|
|
4307
4382
|
text = (_last_obs.get("text") or "").strip()
|
|
4308
4383
|
if text:
|
|
4309
4384
|
_log(f"max-steps fallback (last tool obs) → {text[:80]}")
|
|
4310
|
-
#
|
|
4311
|
-
#
|
|
4312
|
-
#
|
|
4313
|
-
#
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4385
|
+
# Summarize WHAT WAS TRIED + the current blocker, not just the last tool
|
|
4386
|
+
# observation (task #79): a bare last obs ('Stopped npm run dev') reads as a
|
|
4387
|
+
# non-answer and hides the loop the run got stuck in. Build a compact bullet
|
|
4388
|
+
# list of the tool calls from this turn's step trace + the last result.
|
|
4389
|
+
_tried = []
|
|
4390
|
+
for _st in _turn_steps[-8:]:
|
|
4391
|
+
_did = str(_st.get("did") or "").strip().replace("\n", " ")
|
|
4392
|
+
_m = re.search(r"([a-z_]+\([^\n]{0,70})", _did)
|
|
4393
|
+
_label = (_m.group(1) if _m else _did)[:70].strip()
|
|
4394
|
+
if _label:
|
|
4395
|
+
_tried.append(f" • {_label}")
|
|
4396
|
+
_summary = "I ran out of steps before finishing this."
|
|
4397
|
+
if _tried:
|
|
4398
|
+
_summary += " Here's what I tried:\n" + "\n".join(_tried)
|
|
4399
|
+
_summary += "\n\nCurrent blocker (last result):\n" + text[:600]
|
|
4400
|
+
_summary += "\n\nReply 'continue' to have me keep going from here."
|
|
4401
|
+
text = _summary
|
|
4318
4402
|
else:
|
|
4319
4403
|
# No usable tool output (e.g. every step's code failed to parse).
|
|
4320
4404
|
# Don't dead-end on a canned apology — answer from the conversation.
|
|
@@ -4425,6 +4509,10 @@ def run_agent_chat(cfg):
|
|
|
4425
4509
|
out = f"{root} is a file ({os.path.getsize(root)} bytes)."
|
|
4426
4510
|
_last_obs["text"] = out
|
|
4427
4511
|
return out
|
|
4512
|
+
_rb = _read_repeat_block(root)
|
|
4513
|
+
if _rb:
|
|
4514
|
+
_last_obs["text"] = _rb
|
|
4515
|
+
return _rb
|
|
4428
4516
|
entries = []
|
|
4429
4517
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
4430
4518
|
dirnames[:] = [d for d in dirnames if d not in _RAG_SKIP_DIRS]
|
|
@@ -4467,6 +4555,10 @@ def run_agent_chat(cfg):
|
|
|
4467
4555
|
msg = "Error: file too large to read (> 5 MB)."
|
|
4468
4556
|
_last_obs["text"] = msg
|
|
4469
4557
|
return msg
|
|
4558
|
+
_rb = _read_repeat_block(rp)
|
|
4559
|
+
if _rb:
|
|
4560
|
+
_last_obs["text"] = _rb
|
|
4561
|
+
return _rb
|
|
4470
4562
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
4471
4563
|
data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
|
|
4472
4564
|
out = _ws_cap_obs(data or "(empty file)")
|
|
@@ -4800,12 +4892,26 @@ def run_agent_chat(cfg):
|
|
|
4800
4892
|
try:
|
|
4801
4893
|
rp = _ws_write_allowed((path or "").strip())
|
|
4802
4894
|
import os
|
|
4895
|
+
import hashlib as _hl
|
|
4803
4896
|
existed = os.path.exists(rp)
|
|
4804
4897
|
if existed and not overwrite:
|
|
4805
4898
|
msg = (f"Error: {path} already exists — pass overwrite=True to replace the "
|
|
4806
4899
|
"whole file, or use edit_lines/edit_file for a partial change.")
|
|
4807
4900
|
_last_obs["text"] = msg
|
|
4808
4901
|
return msg
|
|
4902
|
+
# Oscillation guard (task #79): if the model rewrites a file to a version it
|
|
4903
|
+
# ALREADY wrote this run (the NextResponse ↔ new Response flip-flop), that
|
|
4904
|
+
# exact content already failed — refuse instead of looping on a non-fix.
|
|
4905
|
+
_ch = _hl.sha1((content or "").encode("utf-8", "replace")).hexdigest()
|
|
4906
|
+
_seen = _file_writes.setdefault(rp, set())
|
|
4907
|
+
if existed and _ch in _seen:
|
|
4908
|
+
msg = (f"You already wrote this EXACT content to {os.path.basename(rp)} "
|
|
4909
|
+
"earlier this task and it did not fix the problem — writing it "
|
|
4910
|
+
"again won't help. Do NOT rewrite this file with the same content. "
|
|
4911
|
+
"Read the server error log, try a genuinely DIFFERENT fix, or call "
|
|
4912
|
+
"final_answer honestly stating what's still failing.")
|
|
4913
|
+
_last_obs["text"] = msg
|
|
4914
|
+
return msg
|
|
4809
4915
|
# Snapshot BEFORE overwriting so revert restores the prior version. Record as
|
|
4810
4916
|
# an "edit" (not "create") when the file already existed, so revert restores
|
|
4811
4917
|
# the backup instead of deleting a file the user may have had before.
|
|
@@ -4820,6 +4926,7 @@ def run_agent_chat(cfg):
|
|
|
4820
4926
|
("Rewrote file — " if existed else "Added ") + f"{len(content_ls)} line(s)",
|
|
4821
4927
|
added=[(i + 1, t) for i, t in enumerate(content_ls)],
|
|
4822
4928
|
)})
|
|
4929
|
+
_seen.add(_ch) # remember this content so a later revert-to-it is caught
|
|
4823
4930
|
out = f"{'Overwrote' if existed else 'Created'} {rp} ({len(content)} chars)."
|
|
4824
4931
|
_last_obs["text"] = out
|
|
4825
4932
|
return out
|
|
@@ -4938,6 +5045,23 @@ def run_agent_chat(cfg):
|
|
|
4938
5045
|
_last_obs["text"] = msg
|
|
4939
5046
|
return msg
|
|
4940
5047
|
# else: destructive but non-interactive → allowed by default (unchanged)
|
|
5048
|
+
# Don't spawn a DUPLICATE of a server that's already running (task #79): the
|
|
5049
|
+
# model re-runs 'npm run dev' and Next/Vite then bind a SECOND port, which
|
|
5050
|
+
# confused the whole debug loop (3000 500 vs 3001 refused). If the identical
|
|
5051
|
+
# command+workdir is still listening, reuse it.
|
|
5052
|
+
for _srv in _ws_load_servers():
|
|
5053
|
+
if _srv.get("command") == command and _srv.get("workdir") == wd:
|
|
5054
|
+
_live = _ws_listening_ports(_srv.get("pid") or 0)
|
|
5055
|
+
if _live:
|
|
5056
|
+
_urls = ", ".join(f"http://localhost:{p}" for p in _live)
|
|
5057
|
+
msg = (f"'{command}' is ALREADY running — {_urls} (pid "
|
|
5058
|
+
f"{_srv.get('pid')}). Reusing it; do NOT start another. A "
|
|
5059
|
+
"dev server hot-reloads code changes automatically, so just "
|
|
5060
|
+
f"request {_urls} again. Use stop_server first only if you "
|
|
5061
|
+
"truly need a full restart.")
|
|
5062
|
+
_emit({"type": "step", "text": f"Server already up → {_urls} (reused)"})
|
|
5063
|
+
_last_obs["text"] = msg
|
|
5064
|
+
return msg
|
|
4941
5065
|
_emit({"type": "step", "text": f"Running → {command[:70]}"})
|
|
4942
5066
|
t = max(5, min(int(timeout_seconds or 180), 600))
|
|
4943
5067
|
# Own process group (start_new_session) + output to a log file. This is
|
|
@@ -5275,8 +5399,38 @@ def run_agent_chat(cfg):
|
|
|
5275
5399
|
_log(f"compact system prompt unavailable ({_e}); using smolagents default")
|
|
5276
5400
|
agent = _ToolAgent(**agent_kwargs)
|
|
5277
5401
|
_agent_ref["a"] = agent # let step_callback trim this agent's step memory (#63)
|
|
5402
|
+
# At the step budget the run ends WITHOUT final_answer (_maxsteps_partial hit). Offer
|
|
5403
|
+
# to keep going instead of dead-ending (task #80): a terminal user says Yes → extend
|
|
5404
|
+
# the budget to a large ceiling and CONTINUE with the existing memory (reset=False,
|
|
5405
|
+
# so the model keeps all prior observations). smolagents captures max_steps as a
|
|
5406
|
+
# LOCAL at run() time, so we can't bump it mid-loop — we re-enter run() with a raised
|
|
5407
|
+
# agent.max_steps instead. The real bound is the 30-min worker job cap, so 1000 just
|
|
5408
|
+
# means "run to completion". 'Yes, don't ask again' is handled REPL-side: it flips a
|
|
5409
|
+
# session flag so the NEXT turn arrives with alwaysExtendOnMaxStep and we auto-extend
|
|
5410
|
+
# without prompting.
|
|
5411
|
+
_MAXSTEP_CEILING = 1000
|
|
5412
|
+
_always_extend = bool(cfg.get("alwaysExtendOnMaxStep"))
|
|
5278
5413
|
with contextlib.redirect_stdout(sys.stderr):
|
|
5279
5414
|
result = agent.run(task_with_hint)
|
|
5415
|
+
while _maxsteps_partial.get("hit"):
|
|
5416
|
+
if _always_extend:
|
|
5417
|
+
_log(f"max-steps reached → auto-extending to {_MAXSTEP_CEILING} "
|
|
5418
|
+
"(don't-ask-again session flag)")
|
|
5419
|
+
_do_extend = True
|
|
5420
|
+
elif _interactive_approval and _job_id and pdf_api_base and pdf_worker_key:
|
|
5421
|
+
_do_extend = _ws_request_approval(
|
|
5422
|
+
pdf_api_base, pdf_worker_key, _job_id,
|
|
5423
|
+
f"__MAXSTEP__::Reached the step budget ({agent.max_steps}). "
|
|
5424
|
+
"Keep going?", "max-steps")
|
|
5425
|
+
else:
|
|
5426
|
+
_do_extend = False # web / no interactive channel → current wrap-up
|
|
5427
|
+
if not _do_extend:
|
|
5428
|
+
break
|
|
5429
|
+
_maxsteps_partial["hit"] = False
|
|
5430
|
+
agent.max_steps = _MAXSTEP_CEILING
|
|
5431
|
+
_emit({"type": "step", "text": "Continuing — step budget extended…"})
|
|
5432
|
+
_log(f"continuing run with raised budget {agent.max_steps}")
|
|
5433
|
+
result = agent.run(task_with_hint, reset=False)
|
|
5280
5434
|
# Final formatting — NO extra summarizer model call. In multi-step mode the
|
|
5281
5435
|
# agent's own final_answer() (or the max-steps fallback above) already holds the
|
|
5282
5436
|
# synthesized answer; we just strip the internal hint tags we appended to tool
|
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.247",
|
|
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",
|