@tiens.nguyen/gonext-local-worker 1.0.256 β 1.0.258
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 +15 -0
- package/gonext_agent_chat.py +207 -5
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -803,7 +803,22 @@ function onApprovalKey(key) {
|
|
|
803
803
|
// again this session" choice (the caller then stops sending the prompt).
|
|
804
804
|
const MAXSTEP_PREFIX = "__MAXSTEP__::";
|
|
805
805
|
const TIMEOUT_PREFIX = "__TIMEOUT__::";
|
|
806
|
+
const COMPACT_PREFIX = "__COMPACT__::";
|
|
806
807
|
async function approvalPrompt(command) {
|
|
808
|
+
// Context-compaction prompt (task #87 rec#3): the running context got large. Ask whether
|
|
809
|
+
// to aggressively compact older steps. No answer in 30s = No (keep full detail β the safe
|
|
810
|
+
// default; the always-on auto-trim still bounds things).
|
|
811
|
+
if (command.startsWith(COMPACT_PREFIX)) {
|
|
812
|
+
const label = command.slice(COMPACT_PREFIX.length);
|
|
813
|
+
if (!process.stdin.isTTY) return { allow: false, dontAsk: false };
|
|
814
|
+
process.stdout.write("\n" + yellow("π " + label) + dim(" (no answer in 30s = keep full)") + "\n");
|
|
815
|
+
const idx = await pickFromList(
|
|
816
|
+
["Yes, compact older steps", "No, keep full detail"],
|
|
817
|
+
0,
|
|
818
|
+
{ timeoutMs: 30000, timeoutIndex: 1 }
|
|
819
|
+
);
|
|
820
|
+
return { allow: idx === 0, dontAsk: false };
|
|
821
|
+
}
|
|
807
822
|
// Time-budget prompt (task #81): the agent hit its wall-clock budget. Ask whether to
|
|
808
823
|
// keep waiting; auto-answer No after 60s of no input so the run can't hang forever.
|
|
809
824
|
if (command.startsWith(TIMEOUT_PREFIX)) {
|
package/gonext_agent_chat.py
CHANGED
|
@@ -2342,6 +2342,8 @@ def _ws_request_approval(api_base, worker_key, job_id, command, reason):
|
|
|
2342
2342
|
return False
|
|
2343
2343
|
if command.startswith("__MAXSTEP__::"):
|
|
2344
2344
|
_emit({"type": "step", "text": "Awaiting your choice: continue past the step budget?"})
|
|
2345
|
+
elif command.startswith("__COMPACT__::"):
|
|
2346
|
+
_emit({"type": "step", "text": "Awaiting your choice: compact the context?"})
|
|
2345
2347
|
else:
|
|
2346
2348
|
_emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
|
|
2347
2349
|
deadline = _t.time() + 180 # 3 min; absent user β deny (never auto-run something risky)
|
|
@@ -2485,6 +2487,32 @@ def _ws_save_servers(servers: list) -> None:
|
|
|
2485
2487
|
_log(f"servers.json write failed: {e}")
|
|
2486
2488
|
|
|
2487
2489
|
|
|
2490
|
+
def _ws_cap_servers(max_live: int = 3) -> list:
|
|
2491
|
+
"""Server lifecycle (task #90 Phase 2): prune dead registry entries and STOP the
|
|
2492
|
+
oldest background servers beyond max_live, so dev servers leaked from earlier
|
|
2493
|
+
turns can't accumulate and squat every default port (the root cause of the
|
|
2494
|
+
'port 3000/3001/3005 busy' punt seen live). Only ever touches processes in OUR
|
|
2495
|
+
registry (started by run_command β never arbitrary user processes). Persists the
|
|
2496
|
+
pruned registry and returns the live list, oldest first."""
|
|
2497
|
+
import os
|
|
2498
|
+
import signal
|
|
2499
|
+
servers = _ws_load_servers() # prune=True drops already-dead entries
|
|
2500
|
+
if len(servers) > max_live:
|
|
2501
|
+
for s in servers[:-max_live]: # registry is append-ordered β oldest first
|
|
2502
|
+
try:
|
|
2503
|
+
pgid = int(s.get("pgid", 0))
|
|
2504
|
+
if pgid <= 0:
|
|
2505
|
+
continue
|
|
2506
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
2507
|
+
_log(f"server lifecycle: stopped old background server pid={s.get('pid')} "
|
|
2508
|
+
f"ports={s.get('ports')} ('{str(s.get('command'))[:40]}')")
|
|
2509
|
+
except (OSError, ValueError) as e: # already gone / no permission
|
|
2510
|
+
_log(f"server lifecycle: stop skipped pid={s.get('pid')}: {e}")
|
|
2511
|
+
servers = servers[-max_live:]
|
|
2512
|
+
_ws_save_servers(servers)
|
|
2513
|
+
return servers
|
|
2514
|
+
|
|
2515
|
+
|
|
2488
2516
|
def _ws_scrubbed_env() -> dict:
|
|
2489
2517
|
"""Minimal child env for run_command. The worker process env holds secrets
|
|
2490
2518
|
(GONEXT_WORKER_KEY etc. via worker.env) β a repo's test script must NEVER see
|
|
@@ -3121,13 +3149,53 @@ def run_agent_chat(cfg):
|
|
|
3121
3149
|
# the on-disk folder overview change between turns (the agent creates/edits files),
|
|
3122
3150
|
# so they must NOT sit inside the static tool_hint β they'd bust the prefix cache
|
|
3123
3151
|
# mid-reference every turn. They join the variable tail of task_with_hint instead.
|
|
3152
|
+
# Task #90 Phase 2 β environment visibility: prune/cap the background-server
|
|
3153
|
+
# registry once per turn (stops the oldest beyond 3 β kills the leak class), then
|
|
3154
|
+
# SHOW the survivors to the model so a 'port busy' is instantly explicable and
|
|
3155
|
+
# reuse/stop_server is actionable. Variable (server state changes) β tail.
|
|
3156
|
+
_ws_servers_line = ""
|
|
3157
|
+
if _WS_AVAILABLE:
|
|
3158
|
+
try:
|
|
3159
|
+
import os as _os_srv
|
|
3160
|
+
_live_srv = _ws_cap_servers()
|
|
3161
|
+
if _live_srv:
|
|
3162
|
+
_descs = ", ".join(
|
|
3163
|
+
":" + ",".join(str(p) for p in (s.get("ports") or ["?"]))
|
|
3164
|
+
+ f" ('{str(s.get('command'))[:30]}' in "
|
|
3165
|
+
+ f"{_os_srv.path.basename(str(s.get('workdir') or '')) or '?'})"
|
|
3166
|
+
for s in _live_srv)
|
|
3167
|
+
_ws_servers_line = (
|
|
3168
|
+
f" Background servers YOU already started (still running): {_descs}. "
|
|
3169
|
+
"REUSE one if it fits the task, or stop_server(<port>) β their ports "
|
|
3170
|
+
"are TAKEN, so never start a duplicate on one of them.\n")
|
|
3171
|
+
except Exception as _e: # noqa: BLE001
|
|
3172
|
+
_log(f"server context skip: {_e}")
|
|
3124
3173
|
_ws_context_block = (
|
|
3125
3174
|
_ws_current_line
|
|
3126
3175
|
+ f" Other registered workspaces (use only if the user names them): {_ws_names}\n"
|
|
3127
|
-
|
|
3176
|
+
+ _ws_servers_line
|
|
3177
|
+
+ f" Current folder contents:\n"
|
|
3128
3178
|
+ "\n".join(f" {line}" for line in _ws_overview.splitlines()) + "\n"
|
|
3129
3179
|
) if _WS_AVAILABLE else ""
|
|
3130
3180
|
_ws_tool_block = (
|
|
3181
|
+
# Task #90 (root-cause rev): senior-dev autonomy as GENERAL principles β outcome
|
|
3182
|
+
# over exit code, own the environment, evidence before finishing, switch approach.
|
|
3183
|
+
# Static (cached, per #86); deliberately not tied to any specific failure case.
|
|
3184
|
+
"WORK LIKE A SENIOR DEVELOPER β you OWN the task until it DEMONSTRABLY works:\n"
|
|
3185
|
+
" * OUTCOME over exit code: a command exiting 0 is NOT the goal. After any "
|
|
3186
|
+
"state-changing step (start/build/install/edit), VERIFY the intended EFFECT "
|
|
3187
|
+
"(server responds, artifact exists, test green, file contains the change) before "
|
|
3188
|
+
"moving on or finishing.\n"
|
|
3189
|
+
" * On failure: read the error, fix the ROOT CAUSE (edit the code/config, "
|
|
3190
|
+
"install the missing dependency, adjust the environment), re-run to confirm. If "
|
|
3191
|
+
"the SAME fix fails twice, SWITCH APPROACH β never repeat it, never just give up.\n"
|
|
3192
|
+
" * The environment is YOURS to manage: a busy port, missing package, or stale "
|
|
3193
|
+
"process you started earlier is yours to RESOLVE with the tools (e.g. start on a "
|
|
3194
|
+
"free port via 'env VAR=value cmd', stop_server your own old server) β not to "
|
|
3195
|
+
"report back as the user's problem.\n"
|
|
3196
|
+
" * final_answer needs EVIDENCE: say what you verified and how. If a thing "
|
|
3197
|
+
"truly cannot be done with these tools, state plainly WHAT failed and WHY β but "
|
|
3198
|
+
"NEVER end with 'you can do it yourself' when a tool could have done it.\n"
|
|
3131
3199
|
" - list_dir(path) / read_text_file(path) β browse and read files.\n"
|
|
3132
3200
|
" - grep_repo(pattern, path='', glob='') β search code, returns file:line hits. "
|
|
3133
3201
|
"Use this to find the EXACT lines to change.\n"
|
|
@@ -3348,8 +3416,14 @@ def run_agent_chat(cfg):
|
|
|
3348
3416
|
f"(Current date/time: {now_str} β pass a timezone to get_current_datetime() only "
|
|
3349
3417
|
"if the task needs a DIFFERENT one.)\n"
|
|
3350
3418
|
+ _deploy_block
|
|
3351
|
-
+ _ws_context_block
|
|
3352
|
-
|
|
3419
|
+
+ _ws_context_block
|
|
3420
|
+
# Task #90: ~25-token echo of the autonomy contract at the highest-attention spot
|
|
3421
|
+
# (recency β right next to the task), reinforcing the full directive that lives in
|
|
3422
|
+
# the cached reference above. Workspace/coding mode only.
|
|
3423
|
+
+ ("Remember: FINISH it yourself β fix blockers, VERIFY the outcome, then "
|
|
3424
|
+
"final_answer with evidence; never hand the task back.\n"
|
|
3425
|
+
if _WS_AVAILABLE else "")
|
|
3426
|
+
+ "\nTASK (answer THIS, choose the tool that fits it):\n"
|
|
3353
3427
|
f"{task_text}\n"
|
|
3354
3428
|
)
|
|
3355
3429
|
|
|
@@ -3873,6 +3947,81 @@ def run_agent_chat(cfg):
|
|
|
3873
3947
|
# agent's memory to trim old observations (task #63, keeps the prompt from growing
|
|
3874
3948
|
# O(nΒ²) as big file reads pile up in the step trace).
|
|
3875
3949
|
_agent_ref: dict = {"a": None}
|
|
3950
|
+
# Task #87 rec#3: when the context grows large in an INTERACTIVE terminal turn, ask
|
|
3951
|
+
# the user Yes/No before aggressively compacting it (rather than deciding silently).
|
|
3952
|
+
# decision: None = not yet asked this turn; "always" = user said yes, keep compacting
|
|
3953
|
+
# each step without re-asking; "never" = user said no, don't ask again this turn (the
|
|
3954
|
+
# normal auto-trim above still runs). Threshold in prompt tokens for the last request.
|
|
3955
|
+
_compact_state: dict = {"decision": None}
|
|
3956
|
+
_COMPACT_ASK_TOKENS = 14000
|
|
3957
|
+
|
|
3958
|
+
# Task #90 Phase 3 β completion gate (code-enforced, complements the prompt). Fires at
|
|
3959
|
+
# most ONCE per turn: refuse the first final_answer that either hands the task back to
|
|
3960
|
+
# the user, or lands right after a state-changing command that was never verified β with
|
|
3961
|
+
# a verify-or-explain nudge fed back to the model (smolagents final_answer_checks: a
|
|
3962
|
+
# raised message becomes the check-failure the model sees and retries on). After one
|
|
3963
|
+
# nudge it ACCEPTS unconditionally β no loop; an honest "here's what failed" is valid.
|
|
3964
|
+
_completion_gate: dict = {"nudged": False}
|
|
3965
|
+
# Punt phrases: handing the CORE task back ("β¦start it yourself", "you'll need to run").
|
|
3966
|
+
# Deliberately narrow so a helpful tip ("you can also run npm test") does NOT trigger it.
|
|
3967
|
+
_PUNT_RE = re.compile(
|
|
3968
|
+
r"\b(?:do|run|start|complete|finish|build) it yourself\b"
|
|
3969
|
+
r"|\byou can navigate to\b"
|
|
3970
|
+
r"|\bstart (?:it|the (?:app|server|project|dev server)) yourself\b"
|
|
3971
|
+
r"|\byou'?ll (?:have to|need to) (?:run|start|do|build)\b",
|
|
3972
|
+
re.I)
|
|
3973
|
+
|
|
3974
|
+
def _final_answer_gate(final_answer, memory, agent=None):
|
|
3975
|
+
"""smolagents final_answer_check. True = accept; raising = reject with the message
|
|
3976
|
+
the model then sees. Only nudges when there's real evidence of an unfinished job."""
|
|
3977
|
+
if _completion_gate["nudged"]:
|
|
3978
|
+
return True # already nudged once this turn β accept, never loop
|
|
3979
|
+
ans = str(final_answer if final_answer is not None else "")
|
|
3980
|
+
punt = bool(_PUNT_RE.search(ans))
|
|
3981
|
+
# The most-recent tool result: a command that finished but whose EFFECT was never
|
|
3982
|
+
# verified (Phase-1 'EXITED 0 (β¦VERIFYβ¦)' marker), with no server/HTTP confirmation.
|
|
3983
|
+
last_unverified = False
|
|
3984
|
+
try:
|
|
3985
|
+
for _st in reversed(getattr(memory, "steps", []) or []):
|
|
3986
|
+
_obs = getattr(_st, "observations", None)
|
|
3987
|
+
if isinstance(_obs, str) and _obs.strip():
|
|
3988
|
+
last_unverified = ("EXITED 0 (" in _obs) and not any(
|
|
3989
|
+
k in _obs for k in
|
|
3990
|
+
("RUNNING:", "listening", "HTTP 2", "http://localhost"))
|
|
3991
|
+
break
|
|
3992
|
+
except Exception: # noqa: BLE001
|
|
3993
|
+
pass
|
|
3994
|
+
if punt or last_unverified:
|
|
3995
|
+
_completion_gate["nudged"] = True
|
|
3996
|
+
raise ValueError(
|
|
3997
|
+
"NOT DONE YET β you ran commands or changed files but have NOT shown the "
|
|
3998
|
+
"goal actually works. Do ONE of these, then call final_answer:\n"
|
|
3999
|
+
" 1) VERIFY it β curl/open the server, run the test, or re-read the file to "
|
|
4000
|
+
"confirm the change took β and answer WITH that evidence; OR\n"
|
|
4001
|
+
" 2) if it genuinely cannot be done with these tools, state plainly WHAT "
|
|
4002
|
+
"failed and WHY.\n"
|
|
4003
|
+
"Do NOT hand an unfinished task back to the user.")
|
|
4004
|
+
return True
|
|
4005
|
+
|
|
4006
|
+
def _aggressive_compact(steps):
|
|
4007
|
+
"""User-approved deep compaction: keep only the LAST step in full; for every
|
|
4008
|
+
older step shrink the observation to a stub and the model_output to just its
|
|
4009
|
+
Thought head. More aggressive than the always-on auto-trim (keep-last-2). Returns
|
|
4010
|
+
chars saved. Idempotent β already-short fields fall under the caps and are skipped."""
|
|
4011
|
+
saved = 0
|
|
4012
|
+
for _st in steps[:-1]:
|
|
4013
|
+
_obs = getattr(_st, "observations", None)
|
|
4014
|
+
if isinstance(_obs, str) and len(_obs) > 200:
|
|
4015
|
+
_b = len(_obs)
|
|
4016
|
+
_st.observations = _obs[:150].rstrip() + "\nβ¦[compacted β re-read if needed]"
|
|
4017
|
+
saved += _b - len(_st.observations)
|
|
4018
|
+
_mo = getattr(_st, "model_output", None)
|
|
4019
|
+
if isinstance(_mo, str) and len(_mo) > 220:
|
|
4020
|
+
_b = len(_mo)
|
|
4021
|
+
_head = re.split(r"<code[\s>]|```", _mo, maxsplit=1)[0].rstrip()
|
|
4022
|
+
_st.model_output = (_head[:200].rstrip() or _mo[:150].rstrip()) + "\nβ¦[compacted]"
|
|
4023
|
+
saved += _b - len(_st.model_output)
|
|
4024
|
+
return saved
|
|
3876
4025
|
|
|
3877
4026
|
def step_callback(step_log):
|
|
3878
4027
|
step_num = getattr(step_log, "step_number", "?")
|
|
@@ -3933,6 +4082,39 @@ def run_agent_chat(cfg):
|
|
|
3933
4082
|
if _trim_n:
|
|
3934
4083
|
_log(f"context trim: shrank {_trim_n} old step field(s), "
|
|
3935
4084
|
f"saved ~{_trim_saved} chars (#63/#87)")
|
|
4085
|
+
|
|
4086
|
+
# (C) Task #87 rec#3 β INTERACTIVE compaction consent. When the context is
|
|
4087
|
+
# genuinely large and we're in a terminal that can prompt, ask the user
|
|
4088
|
+
# Yes/No before deep-compacting (the auto-trim above is always-on; THIS is
|
|
4089
|
+
# the extra, opt-in aggressive pass). Only in the terminal (interactive) β
|
|
4090
|
+
# web has no picker, so it just keeps the auto-trim.
|
|
4091
|
+
if (_interactive_approval and _job_id and pdf_api_base and pdf_worker_key
|
|
4092
|
+
and _compact_state["decision"] != "never"):
|
|
4093
|
+
# Size of the last request in prompt tokens (exact if the server
|
|
4094
|
+
# reported it, else ~chars/4 of the current step memory).
|
|
4095
|
+
_tu = getattr(step_log, "token_usage", None)
|
|
4096
|
+
_in_tok = int(getattr(_tu, "input_tokens", 0) or 0) if _tu else 0
|
|
4097
|
+
if _in_tok <= 0:
|
|
4098
|
+
_chars = sum(len(getattr(_s, "model_output", "") or "")
|
|
4099
|
+
+ len(getattr(_s, "observations", "") or "")
|
|
4100
|
+
for _s in _steps)
|
|
4101
|
+
_in_tok = _chars // 4 + 7000 # + fixed system/reference baseline
|
|
4102
|
+
if _in_tok >= _COMPACT_ASK_TOKENS and len(_steps) > 2:
|
|
4103
|
+
if _compact_state["decision"] != "always":
|
|
4104
|
+
_kt = _in_tok // 1000
|
|
4105
|
+
_allow = _ws_request_approval(
|
|
4106
|
+
pdf_api_base, pdf_worker_key, _job_id,
|
|
4107
|
+
f"__COMPACT__::The context is getting large (~{_kt}k tokens "
|
|
4108
|
+
"per request). Compact older steps to keep it fast?",
|
|
4109
|
+
"compact-context")
|
|
4110
|
+
_compact_state["decision"] = "always" if _allow else "never"
|
|
4111
|
+
_emit({"type": "step", "text": (
|
|
4112
|
+
"Compacting contextβ¦" if _allow
|
|
4113
|
+
else "Keeping full context (won't ask again this turn).")})
|
|
4114
|
+
if _compact_state["decision"] == "always":
|
|
4115
|
+
_cs = _aggressive_compact(_steps)
|
|
4116
|
+
if _cs:
|
|
4117
|
+
_log(f"context compact (user-approved): saved ~{_cs} chars")
|
|
3936
4118
|
except Exception as _e: # noqa: BLE001
|
|
3937
4119
|
_log(f"memory-trim skip: {_e}")
|
|
3938
4120
|
|
|
@@ -5204,8 +5386,23 @@ def run_agent_chat(cfg):
|
|
|
5204
5386
|
rc = proc.poll()
|
|
5205
5387
|
if rc is not None:
|
|
5206
5388
|
out = _ws_distill_output(_read_log())
|
|
5207
|
-
|
|
5208
|
-
|
|
5389
|
+
# Task #90 Phase 1 β truthful perception: never claim success the
|
|
5390
|
+
# tool didn't observe. exit 0 is a FACT about the process, not
|
|
5391
|
+
# proof of the GOAL (e.g. `npm start` exits 0 when its port is
|
|
5392
|
+
# busy and the server never started β the old "PASSED (exit 0)"
|
|
5393
|
+
# label told the model it succeeded, 3Γ in one live run). The
|
|
5394
|
+
# RUNNING/LAUNCHED paths below stay β those ARE observed outcomes.
|
|
5395
|
+
# General (no command-name or error-string matching): the label
|
|
5396
|
+
# states the fact and hands verification to the model, which the
|
|
5397
|
+
# senior-dev directive tells it to do.
|
|
5398
|
+
status = (
|
|
5399
|
+
"EXITED 0 (the command finished β exit code alone does NOT "
|
|
5400
|
+
"prove the intended effect happened; check the output below "
|
|
5401
|
+
"and VERIFY before relying on it)"
|
|
5402
|
+
if rc == 0 else f"FAILED (exit {rc})"
|
|
5403
|
+
)
|
|
5404
|
+
_emit({"type": "step", "text":
|
|
5405
|
+
f"Command {'finished' if rc == 0 else 'failed'} β {command[:50]}"})
|
|
5209
5406
|
result = f"{status}\n{out}" if out.strip() else status
|
|
5210
5407
|
_last_obs["text"] = result
|
|
5211
5408
|
return result
|
|
@@ -5477,6 +5674,11 @@ def run_agent_chat(cfg):
|
|
|
5477
5674
|
max_steps=max_steps,
|
|
5478
5675
|
step_callbacks=[step_callback],
|
|
5479
5676
|
)
|
|
5677
|
+
# Task #90 Phase 3: the completion gate only makes sense in coding/workspace mode
|
|
5678
|
+
# (state-changing commands + file edits); retrieval turns never trip it anyway, but
|
|
5679
|
+
# gating keeps plain-chat/research answers untouched.
|
|
5680
|
+
if _WS_AVAILABLE:
|
|
5681
|
+
agent_kwargs["final_answer_checks"] = [_final_answer_gate]
|
|
5480
5682
|
if _AgentBase is CodeAgent:
|
|
5481
5683
|
# Only the CodeAgent runs a Python executor; ToolCallingAgent takes neither.
|
|
5482
5684
|
# 660s, NOT 60 (task #74): the wall clock must exceed the LARGEST tool-internal
|
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.258",
|
|
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",
|