@tiens.nguyen/gonext-local-worker 1.0.246 → 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.
@@ -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
- function approvalPrompt(command) {
782
- return new Promise((resolve) => {
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",
@@ -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
- _emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
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)
@@ -5396,8 +5399,38 @@ def run_agent_chat(cfg):
5396
5399
  _log(f"compact system prompt unavailable ({_e}); using smolagents default")
5397
5400
  agent = _ToolAgent(**agent_kwargs)
5398
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"))
5399
5413
  with contextlib.redirect_stdout(sys.stderr):
5400
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)
5401
5434
  # Final formatting — NO extra summarizer model call. In multi-step mode the
5402
5435
  # agent's own final_answer() (or the max-steps fallback above) already holds the
5403
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.246",
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",