@tiens.nguyen/gonext-local-worker 1.0.177 → 1.0.179
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 +3 -1
- package/gonext_agent_chat.py +99 -15
- package/package.json +2 -1
- package/thinking_words.txt +106 -0
package/gonext-local-worker.mjs
CHANGED
|
@@ -1799,6 +1799,9 @@ async function runAgentChatJob(job) {
|
|
|
1799
1799
|
return Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
|
|
1800
1800
|
})
|
|
1801
1801
|
.catch(() => []),
|
|
1802
|
+
// The `gonext` terminal REPL's launch folder — download_file/unzip_file output
|
|
1803
|
+
// lands here (must be a registered workspace; the python side re-validates).
|
|
1804
|
+
activeWorkspace: payload?.activeWorkspace ?? "",
|
|
1802
1805
|
});
|
|
1803
1806
|
// 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
|
|
1804
1807
|
// cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
|
package/gonext-repl.mjs
CHANGED
|
@@ -199,7 +199,9 @@ async function runAgentTurn(history) {
|
|
|
199
199
|
const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
|
|
200
200
|
method: "POST",
|
|
201
201
|
headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
|
|
202
|
-
|
|
202
|
+
// cwd = this terminal's folder → agent puts download/unzip output here (when it's
|
|
203
|
+
// a registered workspace).
|
|
204
|
+
body: JSON.stringify({ messages: history, cwd: resolve(process.cwd()) }),
|
|
203
205
|
});
|
|
204
206
|
const data = await res.json().catch(() => ({}));
|
|
205
207
|
if (!res.ok) {
|
package/gonext_agent_chat.py
CHANGED
|
@@ -37,6 +37,33 @@ import urllib.error
|
|
|
37
37
|
_REAL_STDOUT = sys.stdout
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
# Playful "still thinking" status words shown on the heartbeat while a model call is
|
|
41
|
+
# in flight (see _heartbeat). Loaded once from thinking_words.txt next to this file;
|
|
42
|
+
# a random one is picked per tick so the wait feels alive instead of a fixed string.
|
|
43
|
+
_THINKING_WORDS: list = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _thinking_word() -> str:
|
|
47
|
+
"""A random playful status word (e.g. 'Caffeinating'). Falls back to a small
|
|
48
|
+
built-in list if thinking_words.txt is missing or empty."""
|
|
49
|
+
import os
|
|
50
|
+
import random
|
|
51
|
+
global _THINKING_WORDS
|
|
52
|
+
if not _THINKING_WORDS:
|
|
53
|
+
try:
|
|
54
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thinking_words.txt")
|
|
55
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
56
|
+
_THINKING_WORDS = [
|
|
57
|
+
line.strip() for line in fh
|
|
58
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
59
|
+
]
|
|
60
|
+
except OSError:
|
|
61
|
+
_THINKING_WORDS = []
|
|
62
|
+
if not _THINKING_WORDS:
|
|
63
|
+
_THINKING_WORDS = ["Thinking", "Percolating", "Caffeinating", "Cogitating"]
|
|
64
|
+
return random.choice(_THINKING_WORDS)
|
|
65
|
+
|
|
66
|
+
|
|
40
67
|
def _ssl_context():
|
|
41
68
|
import ssl
|
|
42
69
|
# Disable cert verification — this agent runs locally against dev tunnels
|
|
@@ -1158,6 +1185,17 @@ def _rag_workdir(source_key: str) -> str:
|
|
|
1158
1185
|
return base
|
|
1159
1186
|
|
|
1160
1187
|
|
|
1188
|
+
def _rag_base_dir(source_key: str) -> str:
|
|
1189
|
+
"""Where a URL's downloaded/unzipped files should live. Prefer the active terminal
|
|
1190
|
+
workspace (the folder the `gonext` REPL was opened from) so files land in the
|
|
1191
|
+
user's project; otherwise fall back to the per-URL cache dir under ~/.gonext."""
|
|
1192
|
+
import os
|
|
1193
|
+
if _WS_ACTIVE:
|
|
1194
|
+
os.makedirs(_WS_ACTIVE, exist_ok=True)
|
|
1195
|
+
return _WS_ACTIVE
|
|
1196
|
+
return _rag_workdir(source_key)
|
|
1197
|
+
|
|
1198
|
+
|
|
1161
1199
|
def _rag_download(url: str, dest_path: str = "") -> tuple:
|
|
1162
1200
|
"""Download a public URL with SSRF + size guards — IDEMPOTENT per URL. Every URL
|
|
1163
1201
|
maps to a deterministic work dir (~/.gonext/rag-work/<urlHash>/); relative
|
|
@@ -1168,7 +1206,7 @@ def _rag_download(url: str, dest_path: str = "") -> tuple:
|
|
|
1168
1206
|
src = _rag_gdrive_direct((url or "").strip())
|
|
1169
1207
|
_rag_assert_safe_url(src)
|
|
1170
1208
|
key = _rag_source_key(url)
|
|
1171
|
-
workdir =
|
|
1209
|
+
workdir = _rag_base_dir(key)
|
|
1172
1210
|
dest_path = os.path.expanduser((dest_path or "").strip())
|
|
1173
1211
|
if not dest_path:
|
|
1174
1212
|
name = os.path.basename(src.split("?")[0]) or "download.bin"
|
|
@@ -1176,21 +1214,23 @@ def _rag_download(url: str, dest_path: str = "") -> tuple:
|
|
|
1176
1214
|
name += ".zip"
|
|
1177
1215
|
dest_path = os.path.join(workdir, name)
|
|
1178
1216
|
elif not os.path.isabs(dest_path):
|
|
1179
|
-
# Contain relative paths in the
|
|
1217
|
+
# Contain relative paths in the work dir — models pass bare names like
|
|
1180
1218
|
# "project.zip" which would otherwise land in the worker's cwd (the home dir).
|
|
1181
1219
|
dest_path = os.path.join(workdir, dest_path)
|
|
1182
1220
|
# Cache hit 1: the exact target already exists.
|
|
1183
1221
|
if os.path.isfile(dest_path) and os.path.getsize(dest_path) > 0:
|
|
1184
1222
|
return dest_path, True
|
|
1185
|
-
# Cache hit 2: this URL was downloaded before under a different name —
|
|
1186
|
-
#
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1223
|
+
# Cache hit 2: this URL was downloaded before under a different name — ONLY safe in
|
|
1224
|
+
# the per-URL cache dir (one URL per dir). In an active workspace the folder holds
|
|
1225
|
+
# many unrelated files, so a bare "any zip = this URL" match would be wrong.
|
|
1226
|
+
if not _WS_ACTIVE:
|
|
1227
|
+
try:
|
|
1228
|
+
for fn in sorted(os.listdir(workdir)):
|
|
1229
|
+
p = os.path.join(workdir, fn)
|
|
1230
|
+
if os.path.isfile(p) and fn.lower().endswith(".zip") and os.path.getsize(p) > 0:
|
|
1231
|
+
return p, True
|
|
1232
|
+
except OSError:
|
|
1233
|
+
pass
|
|
1194
1234
|
os.makedirs(os.path.dirname(dest_path) or ".", exist_ok=True)
|
|
1195
1235
|
req = urllib.request.Request(src, headers={"User-Agent": "gonext-rag/1.0"})
|
|
1196
1236
|
total = 0
|
|
@@ -1220,8 +1260,15 @@ def _rag_resolve_zip(zip_path: str) -> str:
|
|
|
1220
1260
|
if os.path.isfile(zp):
|
|
1221
1261
|
return zp
|
|
1222
1262
|
if not os.path.isabs(zp):
|
|
1263
|
+
base = os.path.basename(zp)
|
|
1264
|
+
# Look in the active terminal workspace first (that's where downloads land now),
|
|
1265
|
+
# then fall back to the per-URL cache dirs.
|
|
1266
|
+
if _WS_ACTIVE:
|
|
1267
|
+
cand = os.path.join(_WS_ACTIVE, base)
|
|
1268
|
+
if os.path.isfile(cand):
|
|
1269
|
+
return cand
|
|
1223
1270
|
hits = _glob.glob(os.path.join(
|
|
1224
|
-
os.path.expanduser("~"), ".gonext", "rag-work", "*",
|
|
1271
|
+
os.path.expanduser("~"), ".gonext", "rag-work", "*", base))
|
|
1225
1272
|
if hits:
|
|
1226
1273
|
return hits[0]
|
|
1227
1274
|
return zp
|
|
@@ -1424,6 +1471,12 @@ def _rag_parse_location(loc: str):
|
|
|
1424
1471
|
# require _ws_write_allowed (workspace root + not inside .git/).
|
|
1425
1472
|
_WS_ROOTS: list = [] # [{"name": str, "path": realpath str, "allowRun": bool}]
|
|
1426
1473
|
|
|
1474
|
+
# The active terminal workspace: the folder the `gonext` REPL was launched from
|
|
1475
|
+
# (passed through as payload.activeWorkspace). When set, download_file/unzip_file put
|
|
1476
|
+
# their output HERE instead of the shared ~/.gonext/rag-work cache, so files land in
|
|
1477
|
+
# the user's actual project folder. Only honoured if it's a registered workspace root.
|
|
1478
|
+
_WS_ACTIVE: str = ""
|
|
1479
|
+
|
|
1427
1480
|
|
|
1428
1481
|
def _rag_read_allowed(path: str) -> str:
|
|
1429
1482
|
"""Resolve `path` and ensure it stays within a safe root (worker cwd, temp dir,
|
|
@@ -1437,6 +1490,10 @@ def _rag_read_allowed(path: str) -> str:
|
|
|
1437
1490
|
os.path.realpath(tempfile.gettempdir()),
|
|
1438
1491
|
os.path.realpath(os.path.join(os.path.expanduser("~"), ".gonext")),
|
|
1439
1492
|
] + [w["path"] for w in _WS_ROOTS]
|
|
1493
|
+
# The active terminal folder (download/unzip target) — allow reading what we just
|
|
1494
|
+
# extracted there, even when it isn't a registered workspace.
|
|
1495
|
+
if _WS_ACTIVE:
|
|
1496
|
+
roots.append(_WS_ACTIVE)
|
|
1440
1497
|
if any(rp == r or rp.startswith(r + os.sep) for r in roots):
|
|
1441
1498
|
return rp
|
|
1442
1499
|
raise ValueError(
|
|
@@ -1741,7 +1798,7 @@ def run_agent_chat(cfg):
|
|
|
1741
1798
|
# Registered on the Mac via `gonext-local-worker workspace add <path>`; the worker
|
|
1742
1799
|
# passes the registry through cfg. Only these roots are writable — the security
|
|
1743
1800
|
# boundary for the coding tools.
|
|
1744
|
-
global _WS_ROOTS
|
|
1801
|
+
global _WS_ROOTS, _WS_ACTIVE
|
|
1745
1802
|
_WS_ROOTS = []
|
|
1746
1803
|
for w in (cfg.get("workspaces") or []):
|
|
1747
1804
|
try:
|
|
@@ -1756,6 +1813,31 @@ def run_agent_chat(cfg):
|
|
|
1756
1813
|
except Exception: # noqa: BLE001
|
|
1757
1814
|
continue
|
|
1758
1815
|
_WS_AVAILABLE = bool(_WS_ROOTS)
|
|
1816
|
+
# Active terminal workspace: the folder the `gonext` REPL was launched from (sent as
|
|
1817
|
+
# payload.activeWorkspace = the terminal's cwd). download_file / unzip_file put their
|
|
1818
|
+
# output HERE instead of the shared ~/.gonext/rag-work cache, so files land where the
|
|
1819
|
+
# user opened the terminal. Honoured for any real directory the user explicitly opened
|
|
1820
|
+
# the terminal in — these tools already write outside registered workspaces (to
|
|
1821
|
+
# ~/.gonext), so this is not a new write boundary; the code-EDIT tools stay gated on
|
|
1822
|
+
# _WS_ROOTS (registered workspaces) via _ws_write_allowed, unchanged.
|
|
1823
|
+
_WS_ACTIVE = ""
|
|
1824
|
+
_aw_raw = str(cfg.get("activeWorkspace") or "").strip()
|
|
1825
|
+
if _aw_raw:
|
|
1826
|
+
try:
|
|
1827
|
+
import os as _os
|
|
1828
|
+
aw = _os.path.realpath(_os.path.expanduser(_aw_raw))
|
|
1829
|
+
if _os.path.isdir(aw):
|
|
1830
|
+
_WS_ACTIVE = aw
|
|
1831
|
+
registered = any(
|
|
1832
|
+
aw == w["path"] or aw.startswith(w["path"] + _os.sep) for w in _WS_ROOTS
|
|
1833
|
+
)
|
|
1834
|
+
_log(f"active workspace: {aw} (download/unzip output → here"
|
|
1835
|
+
f"{'' if registered else '; not a registered workspace — reads/writes here limited to download/unzip'})")
|
|
1836
|
+
else:
|
|
1837
|
+
_log(f"active workspace ignored (not a directory): {aw}")
|
|
1838
|
+
except Exception as _e: # noqa: BLE001
|
|
1839
|
+
_WS_ACTIVE = ""
|
|
1840
|
+
_log(f"active workspace ignored (error resolving '{_aw_raw}'): {_e}")
|
|
1759
1841
|
if _WS_AVAILABLE:
|
|
1760
1842
|
_log(f"workspaces: {[(w['name'], w['path'], w['allowRun']) for w in _WS_ROOTS]}")
|
|
1761
1843
|
# Coding tasks are edit→test→fix loops — 5 steps is too tight. Bump the default
|
|
@@ -2726,9 +2808,11 @@ def run_agent_chat(cfg):
|
|
|
2726
2808
|
waited = 0
|
|
2727
2809
|
while not hb_stop.wait(45):
|
|
2728
2810
|
waited += 45
|
|
2811
|
+
# A random playful word each tick (e.g. "Caffeinating… (90s)") instead
|
|
2812
|
+
# of a fixed "still working" line — a large or cold-loading model can
|
|
2813
|
+
# legitimately take a few minutes on prompt-eval.
|
|
2729
2814
|
_emit({"type": "step",
|
|
2730
|
-
"text": f"
|
|
2731
|
-
"cold-loading model can take a few minutes)"})
|
|
2815
|
+
"text": f"{_thinking_word()}… ({waited}s)"})
|
|
2732
2816
|
_log(f"heartbeat: model call in flight {waited}s")
|
|
2733
2817
|
|
|
2734
2818
|
hb = threading.Thread(target=_heartbeat, daemon=True)
|
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.179",
|
|
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",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"gonext_agent_chat.py",
|
|
29
29
|
"gonext_transcribe.py",
|
|
30
30
|
"gonext_mlx_embed.py",
|
|
31
|
+
"thinking_words.txt",
|
|
31
32
|
"README.md",
|
|
32
33
|
"launchd/"
|
|
33
34
|
],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Playful "still thinking / still working" status words for the agent heartbeat.
|
|
2
|
+
# One word/phrase per line; blank lines and lines starting with # are ignored.
|
|
3
|
+
# The worker appends "…" and the elapsed seconds, e.g. "Caffeinating… (90s)".
|
|
4
|
+
Collating
|
|
5
|
+
Caffeinating
|
|
6
|
+
Percolating
|
|
7
|
+
Marinating
|
|
8
|
+
Ruminating
|
|
9
|
+
Cogitating
|
|
10
|
+
Noodling
|
|
11
|
+
Pondering
|
|
12
|
+
Contemplating
|
|
13
|
+
Mulling
|
|
14
|
+
Brewing
|
|
15
|
+
Simmering
|
|
16
|
+
Steeping
|
|
17
|
+
Distilling
|
|
18
|
+
Fermenting
|
|
19
|
+
Incubating
|
|
20
|
+
Marshalling
|
|
21
|
+
Wrangling
|
|
22
|
+
Herding electrons
|
|
23
|
+
Untangling
|
|
24
|
+
Unknotting
|
|
25
|
+
Defragmenting
|
|
26
|
+
Reticulating splines
|
|
27
|
+
Compiling thoughts
|
|
28
|
+
Buffering brilliance
|
|
29
|
+
Warming up the neurons
|
|
30
|
+
Spinning up
|
|
31
|
+
Booting the big brain
|
|
32
|
+
Consulting the oracle
|
|
33
|
+
Summoning tokens
|
|
34
|
+
Conjuring
|
|
35
|
+
Divining
|
|
36
|
+
Calibrating
|
|
37
|
+
Recalibrating
|
|
38
|
+
Triangulating
|
|
39
|
+
Extrapolating
|
|
40
|
+
Interpolating
|
|
41
|
+
Synthesizing
|
|
42
|
+
Crystallizing
|
|
43
|
+
Coalescing
|
|
44
|
+
Assembling
|
|
45
|
+
Stitching
|
|
46
|
+
Weaving
|
|
47
|
+
Knitting
|
|
48
|
+
Threading the needle
|
|
49
|
+
Connecting the dots
|
|
50
|
+
Chasing the thread
|
|
51
|
+
Following the breadcrumbs
|
|
52
|
+
Sifting
|
|
53
|
+
Sieving
|
|
54
|
+
Panning for gold
|
|
55
|
+
Mining
|
|
56
|
+
Excavating
|
|
57
|
+
Spelunking
|
|
58
|
+
Deep-diving
|
|
59
|
+
Surfacing insights
|
|
60
|
+
Percolating harder
|
|
61
|
+
Steeping the tea
|
|
62
|
+
Kneading the dough
|
|
63
|
+
Proofing the loaf
|
|
64
|
+
Letting it rise
|
|
65
|
+
Whisking
|
|
66
|
+
Folding in the facts
|
|
67
|
+
Reducing the sauce
|
|
68
|
+
Plating up
|
|
69
|
+
Garnishing
|
|
70
|
+
Sharpening the pencils
|
|
71
|
+
Oiling the gears
|
|
72
|
+
Greasing the cogs
|
|
73
|
+
Winding the springs
|
|
74
|
+
Charging the flux capacitor
|
|
75
|
+
Aligning the stars
|
|
76
|
+
Consulting the tea leaves
|
|
77
|
+
Reading the room
|
|
78
|
+
Gathering wool
|
|
79
|
+
Chewing it over
|
|
80
|
+
Sleeping on it
|
|
81
|
+
Doing the math
|
|
82
|
+
Carrying the one
|
|
83
|
+
Crunching numbers
|
|
84
|
+
Tallying
|
|
85
|
+
Counting sheep
|
|
86
|
+
Herding cats
|
|
87
|
+
Nudging pixels
|
|
88
|
+
Polishing
|
|
89
|
+
Buffing
|
|
90
|
+
Tuning the antenna
|
|
91
|
+
Finding the signal
|
|
92
|
+
Clearing the static
|
|
93
|
+
Untwisting
|
|
94
|
+
Loosening the knot
|
|
95
|
+
Priming the pump
|
|
96
|
+
Stoking the furnace
|
|
97
|
+
Kindling
|
|
98
|
+
Fanning the embers
|
|
99
|
+
Gathering steam
|
|
100
|
+
Building momentum
|
|
101
|
+
Hatching a plan
|
|
102
|
+
Plotting a course
|
|
103
|
+
Charting the map
|
|
104
|
+
Threading the maze
|
|
105
|
+
Almost there
|
|
106
|
+
Nearly cooked
|