@tiens.nguyen/gonext-local-worker 1.0.223 → 1.0.225
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_agent_chat.py +184 -18
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -2369,6 +2369,23 @@ def _ws_distill_output(out: str, cap_head: int = 1200, cap_tail: int = 4000) ->
|
|
|
2369
2369
|
+ out[-cap_tail:])
|
|
2370
2370
|
|
|
2371
2371
|
|
|
2372
|
+
def _ws_cap_obs(text: str, cap: int = 3000) -> str:
|
|
2373
|
+
"""Cap a READ tool's observation (read_file_lines / list_dir / read_text_file /
|
|
2374
|
+
grep_repo) before it's returned to the CodeAgent. Every observation stays in the
|
|
2375
|
+
agent's step memory and is re-sent to the model EVERY subsequent step, so an
|
|
2376
|
+
uncapped 20k-char file dump costs ~5-6k tokens PER remaining step (O(n²) growth —
|
|
2377
|
+
task #63). Keeps the head (line numbers / range start) + a small tail; the model is
|
|
2378
|
+
told to read smaller ranges / grep to locate instead of whole files."""
|
|
2379
|
+
if len(text) <= cap:
|
|
2380
|
+
return text
|
|
2381
|
+
head = int(cap * 0.7)
|
|
2382
|
+
tail = cap - head
|
|
2383
|
+
return (text[:head].rstrip()
|
|
2384
|
+
+ f"\n…[{len(text) - cap} chars truncated to save context — read a smaller "
|
|
2385
|
+
"line range, or grep_repo to locate the exact spot]…\n"
|
|
2386
|
+
+ text[-tail:].lstrip())
|
|
2387
|
+
|
|
2388
|
+
|
|
2372
2389
|
def _rag_s3_client(region: str, akid: str, secret: str):
|
|
2373
2390
|
import boto3 # noqa: PLC0415
|
|
2374
2391
|
return boto3.client(
|
|
@@ -2928,7 +2945,9 @@ def run_agent_chat(cfg):
|
|
|
2928
2945
|
" - grep_repo(pattern, path='', glob='') — search code, returns file:line hits. "
|
|
2929
2946
|
"Use this to find the EXACT lines to change.\n"
|
|
2930
2947
|
" - read_file_lines(path, start_line, end_line) — read WITH line numbers "
|
|
2931
|
-
"before editing
|
|
2948
|
+
"before editing. Read a SMALL range around the spot you need (large reads are "
|
|
2949
|
+
"truncated and slow every later step). Use grep_repo to LOCATE first, then read "
|
|
2950
|
+
"~40 lines around the hit — do NOT re-read the whole file or the same range twice.\n"
|
|
2932
2951
|
" - edit_lines(path, start_line, end_line, new_content) — PRIMARY edit tool: "
|
|
2933
2952
|
"replace a line range (backup saved automatically).\n"
|
|
2934
2953
|
" - edit_file(path, old_string, new_string) — replace one exact unique string "
|
|
@@ -2944,18 +2963,15 @@ def run_agent_chat(cfg):
|
|
|
2944
2963
|
f" ALLOWED runners (first word only): {', '.join(sorted(_WS_RUN_ALLOWED))}. "
|
|
2945
2964
|
"Anything else is rejected — do NOT retry a blocked command a different way. "
|
|
2946
2965
|
"For Python packages use `python3 -m pip install <pkg>` (bare `pip` is NOT allowed).\n"
|
|
2947
|
-
"
|
|
2948
|
-
"
|
|
2949
|
-
"
|
|
2950
|
-
"
|
|
2951
|
-
"
|
|
2952
|
-
"
|
|
2953
|
-
"
|
|
2954
|
-
"
|
|
2955
|
-
"
|
|
2956
|
-
"'a terminal is required') — deploy to a directory the remote user can write, "
|
|
2957
|
-
"and if a root step is unavoidable (mv to /var/www, nginx config/reload), "
|
|
2958
|
-
"final_answer the exact one-liner for the user to run on the server themselves.\n"
|
|
2966
|
+
" - deploy_web(local_dir, host, user, remote_path, domain='') — DEPLOY a built "
|
|
2967
|
+
"static site in ONE call: build first (run_command('npm run build')), then "
|
|
2968
|
+
"deploy_web('my-app/build', host, user, remote_path, domain). It rsyncs over your "
|
|
2969
|
+
"SSH KEY, finishes nginx via passwordless sudo when possible, else writes deploy.sh "
|
|
2970
|
+
"+ returns the exact server commands; if key auth isn't set up it returns the "
|
|
2971
|
+
"one-time `ssh-copy-id` to run. PREFER THIS over hand-writing scp/ssh/rsync.\n"
|
|
2972
|
+
" Raw ssh/scp/rsync are also allowed for other remote work — always KEY auth "
|
|
2973
|
+
"(`-o BatchMode=yes`), NEVER a password in a command/file/reply (no stdin here), "
|
|
2974
|
+
"no paramiko/expect with hardcoded creds; remote sudo fails without a TTY.\n"
|
|
2959
2975
|
" NOTE: plain `import os`/`open()` is blocked — use ONLY these tools for files.\n"
|
|
2960
2976
|
" WRITING FILE CONTENT: multi-line content MUST use \\n for newlines (or a "
|
|
2961
2977
|
"triple-quoted string). NEVER put a raw line break inside a normal \"...\" — it "
|
|
@@ -3472,10 +3488,33 @@ def run_agent_chat(cfg):
|
|
|
3472
3488
|
# _write_turn_checkpoint (inside step_callback below) — see _load_and_clear_turn_
|
|
3473
3489
|
# checkpoint above for how a crashed/interrupted turn gets recovered on the next run.
|
|
3474
3490
|
_turn_steps: list = []
|
|
3491
|
+
# Set right after the agent is built (below) so step_callback can reach the running
|
|
3492
|
+
# agent's memory to trim old observations (task #63, keeps the prompt from growing
|
|
3493
|
+
# O(n²) as big file reads pile up in the step trace).
|
|
3494
|
+
_agent_ref: dict = {"a": None}
|
|
3475
3495
|
|
|
3476
3496
|
def step_callback(step_log):
|
|
3477
3497
|
step_num = getattr(step_log, "step_number", "?")
|
|
3478
3498
|
|
|
3499
|
+
# (B) Trim OLD, LARGE tool observations out of the CodeAgent's step memory so
|
|
3500
|
+
# they aren't re-sent to the model on every subsequent step (the #63 O(n²) token
|
|
3501
|
+
# blow-up: an uncapped read_file_lines dump cost ~5-6k tokens PER remaining step).
|
|
3502
|
+
# Keep the last 2 steps' observations intact (the model just acted on them);
|
|
3503
|
+
# replace older long ones with a short placeholder — the model can re-read.
|
|
3504
|
+
try:
|
|
3505
|
+
_mem = getattr(_agent_ref.get("a"), "memory", None)
|
|
3506
|
+
_steps = getattr(_mem, "steps", None)
|
|
3507
|
+
if _steps and len(_steps) > 2:
|
|
3508
|
+
for _st in _steps[:-2]:
|
|
3509
|
+
_obs = getattr(_st, "observations", None)
|
|
3510
|
+
if isinstance(_obs, str) and len(_obs) > 900:
|
|
3511
|
+
_st.observations = (
|
|
3512
|
+
_obs[:400].rstrip()
|
|
3513
|
+
+ "\n…[earlier observation trimmed to save context — "
|
|
3514
|
+
"re-read the file/dir with a small range if you still need it]")
|
|
3515
|
+
except Exception as _e: # noqa: BLE001
|
|
3516
|
+
_log(f"memory-trim skip: {_e}")
|
|
3517
|
+
|
|
3479
3518
|
# Log what was sent to the model (last message in the conversation).
|
|
3480
3519
|
model_input = getattr(step_log, "model_input_messages", None)
|
|
3481
3520
|
if model_input:
|
|
@@ -4102,7 +4141,8 @@ def run_agent_chat(cfg):
|
|
|
4102
4141
|
break
|
|
4103
4142
|
entries.sort()
|
|
4104
4143
|
more = " …(truncated)" if len(entries) >= 300 else ""
|
|
4105
|
-
out =
|
|
4144
|
+
out = _ws_cap_obs(
|
|
4145
|
+
f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more)
|
|
4106
4146
|
_last_obs["text"] = out
|
|
4107
4147
|
return out
|
|
4108
4148
|
except Exception as e: # noqa: BLE001
|
|
@@ -4131,7 +4171,7 @@ def run_agent_chat(cfg):
|
|
|
4131
4171
|
return msg
|
|
4132
4172
|
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
4133
4173
|
data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
|
|
4134
|
-
out = data or "(empty file)"
|
|
4174
|
+
out = _ws_cap_obs(data or "(empty file)")
|
|
4135
4175
|
_last_obs["text"] = out
|
|
4136
4176
|
return out
|
|
4137
4177
|
except Exception as e: # noqa: BLE001
|
|
@@ -4317,7 +4357,7 @@ def run_agent_chat(cfg):
|
|
|
4317
4357
|
_last_obs["text"] = msg
|
|
4318
4358
|
return msg
|
|
4319
4359
|
more = "\n…(capped at 100 hits)" if len(hits) >= 100 else ""
|
|
4320
|
-
out = f"Matches under {root}:\n" + "\n".join(hits) + more
|
|
4360
|
+
out = _ws_cap_obs(f"Matches under {root}:\n" + "\n".join(hits) + more)
|
|
4321
4361
|
_last_obs["text"] = out
|
|
4322
4362
|
return out
|
|
4323
4363
|
except Exception as e: # noqa: BLE001
|
|
@@ -4342,7 +4382,7 @@ def run_agent_chat(cfg):
|
|
|
4342
4382
|
e = int(end_line or 0) or len(lines)
|
|
4343
4383
|
e = min(e, len(lines), s + 399)
|
|
4344
4384
|
body = "".join(f"{i}: {lines[i - 1]}" for i in range(s, e + 1))
|
|
4345
|
-
out = f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}"
|
|
4385
|
+
out = _ws_cap_obs(f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}")
|
|
4346
4386
|
_last_obs["text"] = out
|
|
4347
4387
|
return out
|
|
4348
4388
|
except Exception as ex: # noqa: BLE001
|
|
@@ -4725,6 +4765,130 @@ def run_agent_chat(cfg):
|
|
|
4725
4765
|
_last_obs["text"] = msg
|
|
4726
4766
|
return msg
|
|
4727
4767
|
|
|
4768
|
+
@tool
|
|
4769
|
+
def deploy_web(local_dir: str, host: str, user: str, remote_path: str,
|
|
4770
|
+
domain: str = "") -> str:
|
|
4771
|
+
"""Deploy a built static site to a remote server in ONE step — prefer this over hand-writing scp/ssh/rsync. rsyncs local_dir to user@host:remote_path over SSH KEY auth, and (if domain is given) tries to finish the nginx setup via PASSWORDLESS sudo; if that needs a password it writes a ready-to-run deploy.sh and returns the exact commands for the user. Uses the user's SSH key only — never a password. If key auth isn't set up it returns the one-time `ssh-copy-id` command to run and stops.
|
|
4772
|
+
|
|
4773
|
+
Args:
|
|
4774
|
+
local_dir: the built folder to upload (e.g. 'my-react-app/build') inside the workspace.
|
|
4775
|
+
host: server hostname (e.g. 'example.com').
|
|
4776
|
+
user: SSH username.
|
|
4777
|
+
remote_path: destination dir on the server (e.g. '/home/<user>/site' or '/var/www/site').
|
|
4778
|
+
domain: optional site domain — when set, also generate/apply the nginx server block.
|
|
4779
|
+
"""
|
|
4780
|
+
try:
|
|
4781
|
+
import os
|
|
4782
|
+
import subprocess
|
|
4783
|
+
src = _rag_read_allowed((local_dir or "").strip())
|
|
4784
|
+
if not os.path.isdir(src):
|
|
4785
|
+
msg = (f"Error: local_dir '{local_dir}' is not a folder — build first "
|
|
4786
|
+
"(e.g. run_command('npm run build')) so the output dir exists.")
|
|
4787
|
+
_last_obs["text"] = msg
|
|
4788
|
+
return msg
|
|
4789
|
+
host = (host or "").strip()
|
|
4790
|
+
user = (user or "").strip()
|
|
4791
|
+
remote_path = (remote_path or "").strip()
|
|
4792
|
+
if not host or not user or not remote_path:
|
|
4793
|
+
msg = "Error: host, user and remote_path are all required."
|
|
4794
|
+
_last_obs["text"] = msg
|
|
4795
|
+
return msg
|
|
4796
|
+
target = f"{user}@{host}"
|
|
4797
|
+
sshopts = ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new",
|
|
4798
|
+
"-o", "ConnectTimeout=8"]
|
|
4799
|
+
env = _ws_scrubbed_env()
|
|
4800
|
+
_emit({"type": "step", "text": f"Deploying → {target}:{remote_path}"})
|
|
4801
|
+
# 1) KEY-auth probe. No password is ever used or requested.
|
|
4802
|
+
probe = subprocess.run(["ssh", *sshopts, target, "true"], env=env,
|
|
4803
|
+
capture_output=True, text=True, timeout=25)
|
|
4804
|
+
if probe.returncode != 0:
|
|
4805
|
+
msg = (
|
|
4806
|
+
f"Can't deploy yet: SSH key auth to {target} isn't set up "
|
|
4807
|
+
f"({(probe.stderr or '').strip()[:160]}). Run this ONCE in your "
|
|
4808
|
+
f"OWN terminal (it will ask for the password a final time):\n"
|
|
4809
|
+
f" ssh-copy-id {target}\n"
|
|
4810
|
+
"Then ask me to deploy again — I'll use the key (no password needed)."
|
|
4811
|
+
)
|
|
4812
|
+
_last_obs["text"] = msg
|
|
4813
|
+
return msg
|
|
4814
|
+
# 2) rsync the build over the key.
|
|
4815
|
+
rsync = subprocess.run(
|
|
4816
|
+
["rsync", "-az", "--delete", "-e", "ssh " + " ".join(sshopts),
|
|
4817
|
+
src.rstrip("/") + "/", f"{target}:{remote_path.rstrip('/')}/"],
|
|
4818
|
+
env=env, capture_output=True, text=True, timeout=600)
|
|
4819
|
+
if rsync.returncode != 0:
|
|
4820
|
+
msg = (f"Error: rsync to {target}:{remote_path} failed "
|
|
4821
|
+
f"({(rsync.stderr or rsync.stdout or '').strip()[:200]}).")
|
|
4822
|
+
_last_obs["text"] = msg
|
|
4823
|
+
return msg
|
|
4824
|
+
uploaded = f"Uploaded {os.path.basename(src)} → {target}:{remote_path}."
|
|
4825
|
+
if not domain.strip():
|
|
4826
|
+
_last_obs["text"] = uploaded
|
|
4827
|
+
return uploaded + " No domain given, so nothing was served — pass domain= to configure nginx."
|
|
4828
|
+
dom = domain.strip()
|
|
4829
|
+
webroot = f"/var/www/{dom}"
|
|
4830
|
+
nginx_conf = (
|
|
4831
|
+
f"server {{\n listen 80;\n server_name {dom};\n"
|
|
4832
|
+
f" root {webroot};\n index index.html;\n"
|
|
4833
|
+
f" location / {{ try_files $uri $uri/ /index.html; }}\n}}\n"
|
|
4834
|
+
)
|
|
4835
|
+
# Remote setup script: move the upload into the webroot + wire up nginx.
|
|
4836
|
+
remote_sh = (
|
|
4837
|
+
"set -e\n"
|
|
4838
|
+
f"mkdir -p {webroot}\n"
|
|
4839
|
+
f"cp -r {remote_path.rstrip('/')}/* {webroot}/\n"
|
|
4840
|
+
f"cat > /etc/nginx/sites-available/{dom} <<'NGINX'\n{nginx_conf}NGINX\n"
|
|
4841
|
+
f"ln -sf /etc/nginx/sites-available/{dom} /etc/nginx/sites-enabled/{dom}\n"
|
|
4842
|
+
"nginx -t\n"
|
|
4843
|
+
"systemctl reload nginx\n"
|
|
4844
|
+
)
|
|
4845
|
+
# 3) Try to finish via PASSWORDLESS sudo (sudo -n succeeds only w/ NOPASSWD).
|
|
4846
|
+
fin = subprocess.run(["ssh", *sshopts, target, "sudo -n bash -s"],
|
|
4847
|
+
input=remote_sh, env=env, capture_output=True,
|
|
4848
|
+
text=True, timeout=120)
|
|
4849
|
+
if fin.returncode == 0:
|
|
4850
|
+
out = f"{uploaded} Configured nginx and reloaded — the site is live at http://{dom}/."
|
|
4851
|
+
_emit({"type": "step", "text": f"Site live → http://{dom}/"})
|
|
4852
|
+
_last_obs["text"] = out
|
|
4853
|
+
return out
|
|
4854
|
+
# 4) sudo needs a password (no TTY here) → write a runnable deploy.sh
|
|
4855
|
+
# (SAME sudo commands as the printed steps) and hand it over.
|
|
4856
|
+
sudo_sh = (
|
|
4857
|
+
"#!/usr/bin/env bash\nset -e\n"
|
|
4858
|
+
f"sudo mkdir -p {webroot}\n"
|
|
4859
|
+
f"sudo cp -r {remote_path.rstrip('/')}/* {webroot}/\n"
|
|
4860
|
+
f"sudo tee /etc/nginx/sites-available/{dom} >/dev/null <<'NGINX'\n"
|
|
4861
|
+
f"{nginx_conf}NGINX\n"
|
|
4862
|
+
f"sudo ln -sf /etc/nginx/sites-available/{dom} /etc/nginx/sites-enabled/{dom}\n"
|
|
4863
|
+
"sudo nginx -t && sudo systemctl reload nginx\n"
|
|
4864
|
+
)
|
|
4865
|
+
sh_local = os.path.join(_default_ws_root(), "deploy.sh")
|
|
4866
|
+
try:
|
|
4867
|
+
with open(sh_local, "w", encoding="utf-8") as fh:
|
|
4868
|
+
fh.write(sudo_sh)
|
|
4869
|
+
_ws_record_change("create", sh_local, None)
|
|
4870
|
+
where = sh_local
|
|
4871
|
+
except OSError:
|
|
4872
|
+
where = "(could not write deploy.sh)"
|
|
4873
|
+
out = (
|
|
4874
|
+
f"{uploaded} The nginx/webroot step needs sudo (no password prompt is "
|
|
4875
|
+
f"possible over SSH). I wrote a runnable script to {where} — copy it to "
|
|
4876
|
+
f"the server and run it (`scp deploy.sh {target}:` then `ssh {target} "
|
|
4877
|
+
f"'bash deploy.sh'`), OR run these on the server yourself:\n"
|
|
4878
|
+
+ "\n".join(" " + ln for ln in sudo_sh.splitlines()[2:])
|
|
4879
|
+
+ f"\nThen {dom} serves the site."
|
|
4880
|
+
)
|
|
4881
|
+
_last_obs["text"] = out
|
|
4882
|
+
return out
|
|
4883
|
+
except subprocess.TimeoutExpired:
|
|
4884
|
+
msg = f"Error: deploy to {host} timed out (server unreachable or slow)."
|
|
4885
|
+
_last_obs["text"] = msg
|
|
4886
|
+
return msg
|
|
4887
|
+
except Exception as ex: # noqa: BLE001
|
|
4888
|
+
msg = f"Error: deploy_web failed: {type(ex).__name__}: {ex}"
|
|
4889
|
+
_last_obs["text"] = msg
|
|
4890
|
+
return msg
|
|
4891
|
+
|
|
4728
4892
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
4729
4893
|
get_current_datetime, create_pdf]
|
|
4730
4894
|
# Only register the PDF reader / email / RAG tools when available — the model
|
|
@@ -4739,7 +4903,8 @@ def run_agent_chat(cfg):
|
|
|
4739
4903
|
rag_index, rag_add, rag_search]
|
|
4740
4904
|
if _WS_AVAILABLE:
|
|
4741
4905
|
agent_tools += [grep_repo, read_file_lines, edit_lines, edit_file,
|
|
4742
|
-
create_file, create_folder, run_command, stop_server
|
|
4906
|
+
create_file, create_folder, run_command, stop_server,
|
|
4907
|
+
deploy_web]
|
|
4743
4908
|
# list_dir/read_text_file are needed for workspace browsing even when RAG
|
|
4744
4909
|
# (S3 indexing) isn't configured.
|
|
4745
4910
|
if not _RAG_AVAILABLE:
|
|
@@ -4778,6 +4943,7 @@ def run_agent_chat(cfg):
|
|
|
4778
4943
|
except Exception as _e: # noqa: BLE001
|
|
4779
4944
|
_log(f"compact system prompt unavailable ({_e}); using smolagents default")
|
|
4780
4945
|
agent = _ToolAgent(**agent_kwargs)
|
|
4946
|
+
_agent_ref["a"] = agent # let step_callback trim this agent's step memory (#63)
|
|
4781
4947
|
with contextlib.redirect_stdout(sys.stderr):
|
|
4782
4948
|
result = agent.run(task_with_hint)
|
|
4783
4949
|
# Final formatting — NO extra summarizer model call. In multi-step mode the
|
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.225",
|
|
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",
|