@tiens.nguyen/gonext-local-worker 1.0.165 → 1.0.170
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 +150 -1
- package/gonext_agent_chat.py +406 -10
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - `gonext-local-worker ws-ping-test` — POST worker ws-ping (needs GONEXT_* env)
|
|
7
7
|
* - `gonext-local-worker simulate-chat [text]` — claim next chat job, push fake reply like the real worker (needs GONEXT_* env)
|
|
8
8
|
* - `gonext-local-worker embed --model <mlx-embed-model> [--port 8085]` — run the MLX embeddings server (RAG); use a separate terminal
|
|
9
|
+
* - `gonext-local-worker workspace add|remove|list …` — manage local code folders the agent may read/edit/test in
|
|
9
10
|
* - `gonext-local-worker` — starts polling loop (claims jobs and runs models)
|
|
10
11
|
*/
|
|
11
12
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -222,6 +223,129 @@ if (args[0] === "ws-ping-test") {
|
|
|
222
223
|
process.exit(0);
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
if (args[0] === "workspace") {
|
|
227
|
+
// Manage the local workspaces the agent may READ and EDIT code in. Registration
|
|
228
|
+
// happens HERE on the Mac (never from the web) — the registry is the security
|
|
229
|
+
// boundary for the agent's coding tools (grep/read/edit/run stay inside these roots).
|
|
230
|
+
// gonext-local-worker workspace add <path> [--name <name>] [--allow-run]
|
|
231
|
+
// gonext-local-worker workspace remove <path-or-name>
|
|
232
|
+
// gonext-local-worker workspace list
|
|
233
|
+
const wsFile = join(homedir(), ".gonext", "workspaces.json");
|
|
234
|
+
const readWs = async () => {
|
|
235
|
+
try {
|
|
236
|
+
const raw = JSON.parse(await readFile(wsFile, "utf8"));
|
|
237
|
+
return Array.isArray(raw?.workspaces) ? raw.workspaces : [];
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const writeWs = async (workspaces) => {
|
|
243
|
+
await mkdir(dirname(wsFile), { recursive: true });
|
|
244
|
+
await writeFile(wsFile, JSON.stringify({ workspaces }, null, 2) + "\n");
|
|
245
|
+
};
|
|
246
|
+
const sub = args[1];
|
|
247
|
+
const { resolve } = await import("node:path");
|
|
248
|
+
const { existsSync, statSync } = await import("node:fs");
|
|
249
|
+
if (sub === "add") {
|
|
250
|
+
let p = args[2] || "";
|
|
251
|
+
if (!p) {
|
|
252
|
+
console.error("Usage: gonext-local-worker workspace add <path> [--name <name>] [--allow-run]");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
if (p.startsWith("~/") || p === "~") p = join(homedir(), p.slice(p.length > 1 ? 2 : 1));
|
|
256
|
+
p = resolve(p);
|
|
257
|
+
if (!existsSync(p) || !statSync(p).isDirectory()) {
|
|
258
|
+
console.error(`[gonext-worker] workspace add: not a directory: ${p}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const nameIdx = args.indexOf("--name");
|
|
262
|
+
const name = nameIdx >= 0 && args[nameIdx + 1] ? args[nameIdx + 1] : p.split("/").filter(Boolean).pop();
|
|
263
|
+
const allowRun = args.includes("--allow-run");
|
|
264
|
+
const list = (await readWs()).filter((w) => w.path !== p);
|
|
265
|
+
list.push({ name, path: p, allowRun, addedAt: new Date().toISOString() });
|
|
266
|
+
await writeWs(list);
|
|
267
|
+
console.log(
|
|
268
|
+
`[gonext-worker] workspace registered: ${name} → ${p}` +
|
|
269
|
+
(allowRun ? " (run-commands ENABLED)" : " (run-commands disabled; add --allow-run to enable tests)")
|
|
270
|
+
);
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
if (sub === "remove") {
|
|
274
|
+
const key = args[2] || "";
|
|
275
|
+
const list = await readWs();
|
|
276
|
+
const next = list.filter((w) => w.path !== key && w.name !== key);
|
|
277
|
+
if (next.length === list.length) {
|
|
278
|
+
console.error(`[gonext-worker] workspace remove: no workspace matches '${key}'`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
await writeWs(next);
|
|
282
|
+
console.log(`[gonext-worker] workspace removed: ${key}`);
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
if (sub === "revert") {
|
|
286
|
+
// Restore the files an agent run changed: copy backups back over edits and delete
|
|
287
|
+
// created files. Uses the manifest the python tools persist per change.
|
|
288
|
+
// gonext-local-worker workspace revert → latest run
|
|
289
|
+
// gonext-local-worker workspace revert <runId> → a specific run (see backups dir)
|
|
290
|
+
const { readdirSync, existsSync: ex } = await import("node:fs");
|
|
291
|
+
const { copyFile, unlink } = await import("node:fs/promises");
|
|
292
|
+
const backupsRoot = join(homedir(), ".gonext", "workspace-backups");
|
|
293
|
+
let runId = args[2] || "";
|
|
294
|
+
if (!runId) {
|
|
295
|
+
const runs = (() => {
|
|
296
|
+
try { return readdirSync(backupsRoot).filter((d) => !d.startsWith(".")).sort(); }
|
|
297
|
+
catch { return []; }
|
|
298
|
+
})();
|
|
299
|
+
if (runs.length === 0) {
|
|
300
|
+
console.error("[gonext-worker] workspace revert: no backup runs found.");
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
runId = runs[runs.length - 1];
|
|
304
|
+
}
|
|
305
|
+
const manifestPath = join(backupsRoot, runId, "manifest.json");
|
|
306
|
+
let manifest;
|
|
307
|
+
try {
|
|
308
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
309
|
+
} catch {
|
|
310
|
+
console.error(`[gonext-worker] workspace revert: no manifest for run ${runId} (${manifestPath})`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
let restored = 0, deleted = 0, failed = 0;
|
|
314
|
+
for (const c of manifest?.changes ?? []) {
|
|
315
|
+
try {
|
|
316
|
+
if (c.action === "edit" && c.backup && ex(c.backup)) {
|
|
317
|
+
await copyFile(c.backup, c.path);
|
|
318
|
+
restored += 1;
|
|
319
|
+
console.log(` restored ${c.path}`);
|
|
320
|
+
} else if (c.action === "create" && ex(c.path)) {
|
|
321
|
+
await unlink(c.path);
|
|
322
|
+
deleted += 1;
|
|
323
|
+
console.log(` deleted ${c.path}`);
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
failed += 1;
|
|
327
|
+
console.error(` FAILED ${c.path}: ${err.message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log(
|
|
331
|
+
`[gonext-worker] workspace revert ${runId}: ${restored} restored, ${deleted} created-file(s) deleted` +
|
|
332
|
+
(failed ? `, ${failed} FAILED` : "") +
|
|
333
|
+
" (backups kept)"
|
|
334
|
+
);
|
|
335
|
+
process.exit(failed ? 1 : 0);
|
|
336
|
+
}
|
|
337
|
+
// default: list
|
|
338
|
+
const list = await readWs();
|
|
339
|
+
if (list.length === 0) {
|
|
340
|
+
console.log("[gonext-worker] no workspaces registered. Add one:\n gonext-local-worker workspace add ~/Projects/my-repo --allow-run");
|
|
341
|
+
} else {
|
|
342
|
+
for (const w of list) {
|
|
343
|
+
console.log(` ${w.name} ${w.path} run:${w.allowRun ? "yes" : "no"}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
|
|
225
349
|
if (args[0] === "embed") {
|
|
226
350
|
// Start the MLX embeddings server (OpenAI-compatible /v1/embeddings) for the RAG
|
|
227
351
|
// feature, in the FOREGROUND of this terminal. Run it in a SEPARATE terminal from the
|
|
@@ -1631,7 +1755,10 @@ async function runAgentChatJob(job) {
|
|
|
1631
1755
|
codingBaseURL: payload?.codingBaseURL ?? "",
|
|
1632
1756
|
codingModelId: payload?.codingModelId ?? "",
|
|
1633
1757
|
tools: payload?.tools ?? ["http_request"],
|
|
1634
|
-
|
|
1758
|
+
// 0 = "not explicitly set": python defaults to 5, but may auto-raise to 12 when
|
|
1759
|
+
// workspaces are registered. Passing a literal 5 here would look user-set and
|
|
1760
|
+
// block that bump.
|
|
1761
|
+
maxSteps: payload?.maxSteps ?? 0,
|
|
1635
1762
|
// Max web_search + fetch_url calls the agent may make per task (user-configurable
|
|
1636
1763
|
// in web Settings → Agent). Default 10; past it the retrieval tools refuse.
|
|
1637
1764
|
researchBudget: payload?.researchBudget ?? 10,
|
|
@@ -1664,6 +1791,14 @@ async function runAgentChatJob(job) {
|
|
|
1664
1791
|
ragEmbedModel: payload?.ragEmbedModel ?? "",
|
|
1665
1792
|
ragEmbedUrl: payload?.ragEmbedUrl ?? "",
|
|
1666
1793
|
ragTopK: payload?.ragTopK ?? 6,
|
|
1794
|
+
// Workspaces the agent may read/edit/test code in — read fresh each job so a
|
|
1795
|
+
// `gonext-local-worker workspace add` applies without restarting the worker.
|
|
1796
|
+
workspaces: await readFile(join(homedir(), ".gonext", "workspaces.json"), "utf8")
|
|
1797
|
+
.then((raw) => {
|
|
1798
|
+
const parsed = JSON.parse(raw);
|
|
1799
|
+
return Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
|
|
1800
|
+
})
|
|
1801
|
+
.catch(() => []),
|
|
1667
1802
|
});
|
|
1668
1803
|
// 30 min max for an agent run: multi-step ReAct on a 14B/31B with a cold prompt
|
|
1669
1804
|
// cache — or a remote Ollama box on slow GPU cold-loading a big model — can run
|
|
@@ -2260,6 +2395,20 @@ async function runLocalHealthJob(job) {
|
|
|
2260
2395
|
modelPaths: modelPathStatus,
|
|
2261
2396
|
}
|
|
2262
2397
|
: undefined,
|
|
2398
|
+
// Registered coding workspaces (names/paths only — no secrets) so the web can
|
|
2399
|
+
// show which folders the agent may read/edit on this Mac.
|
|
2400
|
+
workspaces: await readFile(join(homedir(), ".gonext", "workspaces.json"), "utf8")
|
|
2401
|
+
.then((raw) => {
|
|
2402
|
+
const parsed = JSON.parse(raw);
|
|
2403
|
+
return Array.isArray(parsed?.workspaces)
|
|
2404
|
+
? parsed.workspaces.map((w) => ({
|
|
2405
|
+
name: String(w.name ?? ""),
|
|
2406
|
+
path: String(w.path ?? ""),
|
|
2407
|
+
allowRun: Boolean(w.allowRun),
|
|
2408
|
+
}))
|
|
2409
|
+
: undefined;
|
|
2410
|
+
})
|
|
2411
|
+
.catch(() => undefined),
|
|
2263
2412
|
};
|
|
2264
2413
|
const totalTimeSeconds = (Date.now() - start) / 1000;
|
|
2265
2414
|
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
package/gonext_agent_chat.py
CHANGED
|
@@ -519,6 +519,8 @@ _AGENT_KEYWORDS = re.compile(
|
|
|
519
519
|
r"|external\s+source|external\s+api|external\s+service"
|
|
520
520
|
r"|web\s+service|rest\s+api|rest\s+call"
|
|
521
521
|
r"|download|scrape|crawl|zip|unzip|\.zip|rag|index\s+the|knowledge\s+base|summari[sz]e"
|
|
522
|
+
r"|workspace|fix\s+(?:a\s+|the\s+|this\s+)?bug|refactor|codebase|source\s+code|repo(?:sitory)?"
|
|
523
|
+
r"|modify\s+(?:the\s+)?code|change\s+(?:the\s+)?code|run\s+(?:the\s+)?tests?"
|
|
522
524
|
r"|search|find|look\s*up|lookup|weather|news|latest|current|today|tonight"
|
|
523
525
|
r"|date|time|what\s+day|what\s+time"
|
|
524
526
|
r"|read\s+(?:this\s+|that\s+|the\s+)?(?:page|article|url|link)|open\s+(?:this\s+|that\s+|the\s+)?(?:url|link|page)|contents?\s+of"
|
|
@@ -1417,9 +1419,16 @@ def _rag_parse_location(loc: str):
|
|
|
1417
1419
|
return bucket, prefix.strip("/")
|
|
1418
1420
|
|
|
1419
1421
|
|
|
1422
|
+
# Registered workspace roots (from ~/.gonext/workspaces.json via the worker) — set
|
|
1423
|
+
# per-run by run_agent_chat. Reads are allowed in any of them; WRITES additionally
|
|
1424
|
+
# require _ws_write_allowed (workspace root + not inside .git/).
|
|
1425
|
+
_WS_ROOTS: list = [] # [{"name": str, "path": realpath str, "allowRun": bool}]
|
|
1426
|
+
|
|
1427
|
+
|
|
1420
1428
|
def _rag_read_allowed(path: str) -> str:
|
|
1421
|
-
"""Resolve `path` and ensure it stays within a safe root (worker cwd, temp dir,
|
|
1422
|
-
~/.gonext) — so the agent's file tools can't read
|
|
1429
|
+
"""Resolve `path` and ensure it stays within a safe root (worker cwd, temp dir,
|
|
1430
|
+
~/.gonext, or a registered workspace) — so the agent's file tools can't read
|
|
1431
|
+
arbitrary files like ~/.ssh."""
|
|
1423
1432
|
import os
|
|
1424
1433
|
import tempfile
|
|
1425
1434
|
rp = os.path.realpath(path)
|
|
@@ -1427,14 +1436,115 @@ def _rag_read_allowed(path: str) -> str:
|
|
|
1427
1436
|
os.path.realpath(os.getcwd()),
|
|
1428
1437
|
os.path.realpath(tempfile.gettempdir()),
|
|
1429
1438
|
os.path.realpath(os.path.join(os.path.expanduser("~"), ".gonext")),
|
|
1430
|
-
]
|
|
1439
|
+
] + [w["path"] for w in _WS_ROOTS]
|
|
1431
1440
|
if any(rp == r or rp.startswith(r + os.sep) for r in roots):
|
|
1432
1441
|
return rp
|
|
1433
1442
|
raise ValueError(
|
|
1434
|
-
"Path is outside the allowed working directories (download/unzip outputs
|
|
1443
|
+
"Path is outside the allowed working directories (download/unzip outputs "
|
|
1444
|
+
"or a registered workspace)."
|
|
1435
1445
|
)
|
|
1436
1446
|
|
|
1437
1447
|
|
|
1448
|
+
def _ws_for_path(path: str):
|
|
1449
|
+
"""Return the registered workspace dict containing `path`, else None."""
|
|
1450
|
+
import os
|
|
1451
|
+
rp = os.path.realpath(path)
|
|
1452
|
+
for w in _WS_ROOTS:
|
|
1453
|
+
if rp == w["path"] or rp.startswith(w["path"] + os.sep):
|
|
1454
|
+
return w
|
|
1455
|
+
return None
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def _ws_write_allowed(path: str) -> str:
|
|
1459
|
+
"""Resolve `path` for WRITING: must be inside a registered workspace and never
|
|
1460
|
+
inside its .git/ internals (agents must not corrupt version control)."""
|
|
1461
|
+
import os
|
|
1462
|
+
rp = os.path.realpath(path)
|
|
1463
|
+
w = _ws_for_path(rp)
|
|
1464
|
+
if w is None:
|
|
1465
|
+
raise ValueError(
|
|
1466
|
+
"Writes are only allowed inside a registered workspace "
|
|
1467
|
+
"(register one with: gonext-local-worker workspace add <path>)."
|
|
1468
|
+
)
|
|
1469
|
+
rel = os.path.relpath(rp, w["path"])
|
|
1470
|
+
if rel == ".git" or rel.startswith(".git" + os.sep) or (os.sep + ".git" + os.sep) in (os.sep + rel + os.sep):
|
|
1471
|
+
raise ValueError("Writes inside .git/ are not allowed.")
|
|
1472
|
+
return rp
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
_WS_RUN_ID = None # backup folder for this run, created lazily on first change
|
|
1476
|
+
_WS_CHANGES: list = [] # [{"action": "edit"|"create", "path": str, "backup": str|None}]
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def _ws_backup_root() -> str:
|
|
1480
|
+
"""Backup dir for this run (created lazily). Also home of manifest.json."""
|
|
1481
|
+
global _WS_RUN_ID
|
|
1482
|
+
import os
|
|
1483
|
+
import time as _t
|
|
1484
|
+
if _WS_RUN_ID is None:
|
|
1485
|
+
_WS_RUN_ID = _t.strftime("%Y%m%d-%H%M%S", _t.localtime())
|
|
1486
|
+
root = os.path.join(os.path.expanduser("~"), ".gonext", "workspace-backups", _WS_RUN_ID)
|
|
1487
|
+
os.makedirs(root, exist_ok=True)
|
|
1488
|
+
return root
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def _ws_snapshot(path: str) -> str:
|
|
1492
|
+
"""Copy the file to the run's backup dir before its FIRST edit in this run, so
|
|
1493
|
+
every change is revertible. Returns the backup path ('' if new file)."""
|
|
1494
|
+
import os
|
|
1495
|
+
import shutil
|
|
1496
|
+
if not os.path.isfile(path):
|
|
1497
|
+
return ""
|
|
1498
|
+
for c in _WS_CHANGES: # already snapshotted this run — keep the ORIGINAL version
|
|
1499
|
+
if c["path"] == path and c.get("backup"):
|
|
1500
|
+
return c["backup"]
|
|
1501
|
+
root = _ws_backup_root()
|
|
1502
|
+
dest = os.path.join(root, path.lstrip(os.sep))
|
|
1503
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
1504
|
+
shutil.copy2(path, dest)
|
|
1505
|
+
return dest
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _ws_record_change(action: str, path: str, backup) -> None:
|
|
1509
|
+
"""Track a change AND persist the run manifest so `gonext-local-worker workspace
|
|
1510
|
+
revert` can restore edits / delete created files after the process is gone."""
|
|
1511
|
+
import os
|
|
1512
|
+
_WS_CHANGES.append({"action": action, "path": path, "backup": backup or None})
|
|
1513
|
+
try:
|
|
1514
|
+
root = _ws_backup_root()
|
|
1515
|
+
with open(os.path.join(root, "manifest.json"), "w", encoding="utf-8") as fh:
|
|
1516
|
+
json.dump({"changes": _WS_CHANGES}, fh, indent=2)
|
|
1517
|
+
except OSError as e:
|
|
1518
|
+
_log(f"workspace manifest write failed: {e}")
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
# Command prefixes run_command may execute (first token after shlex split). Build/test
|
|
1522
|
+
# runners only — no shells, no package publishes, no sudo.
|
|
1523
|
+
_WS_RUN_ALLOWED = {
|
|
1524
|
+
"npm", "npx", "yarn", "pnpm", "node", "mvn", "./mvnw", "gradle", "./gradlew",
|
|
1525
|
+
"pytest", "python", "python3", "go", "cargo", "make", "dotnet", "swift",
|
|
1526
|
+
"rspec", "bundle", "php", "composer", "tsc", "jest", "vitest",
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def _ws_scrubbed_env() -> dict:
|
|
1531
|
+
"""Minimal child env for run_command. The worker process env holds secrets
|
|
1532
|
+
(GONEXT_WORKER_KEY etc. via worker.env) — a repo's test script must NEVER see
|
|
1533
|
+
them, so we pass an explicit whitelist instead of inheriting."""
|
|
1534
|
+
import os
|
|
1535
|
+
keep = ("PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "SHELL", "USER", "TERM",
|
|
1536
|
+
"JAVA_HOME", "GOPATH", "CARGO_HOME", "NVM_DIR")
|
|
1537
|
+
return {k: os.environ[k] for k in keep if k in os.environ}
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _ws_distill_output(out: str, cap_head: int = 1200, cap_tail: int = 4000) -> str:
|
|
1541
|
+
"""Cap huge build/test output: failures live at the tail, context at the head."""
|
|
1542
|
+
if len(out) <= cap_head + cap_tail + 200:
|
|
1543
|
+
return out
|
|
1544
|
+
return (out[:cap_head] + f"\n…[{len(out) - cap_head - cap_tail} chars omitted]…\n"
|
|
1545
|
+
+ out[-cap_tail:])
|
|
1546
|
+
|
|
1547
|
+
|
|
1438
1548
|
def _rag_s3_client(region: str, akid: str, secret: str):
|
|
1439
1549
|
import boto3 # noqa: PLC0415
|
|
1440
1550
|
return boto3.client(
|
|
@@ -1627,6 +1737,33 @@ def run_agent_chat(cfg):
|
|
|
1627
1737
|
if rag_enabled and not _boto3_ok:
|
|
1628
1738
|
_log("RAG requested but boto3 is not installed in the worker python — RAG tools disabled")
|
|
1629
1739
|
|
|
1740
|
+
# ---- Workspaces (local folders the agent may read/edit/test code in) ----
|
|
1741
|
+
# Registered on the Mac via `gonext-local-worker workspace add <path>`; the worker
|
|
1742
|
+
# passes the registry through cfg. Only these roots are writable — the security
|
|
1743
|
+
# boundary for the coding tools.
|
|
1744
|
+
global _WS_ROOTS
|
|
1745
|
+
_WS_ROOTS = []
|
|
1746
|
+
for w in (cfg.get("workspaces") or []):
|
|
1747
|
+
try:
|
|
1748
|
+
import os as _os
|
|
1749
|
+
p = _os.path.realpath(_os.path.expanduser(str(w.get("path") or "")))
|
|
1750
|
+
if p and _os.path.isdir(p):
|
|
1751
|
+
_WS_ROOTS.append({
|
|
1752
|
+
"name": str(w.get("name") or _os.path.basename(p)),
|
|
1753
|
+
"path": p,
|
|
1754
|
+
"allowRun": bool(w.get("allowRun")),
|
|
1755
|
+
})
|
|
1756
|
+
except Exception: # noqa: BLE001
|
|
1757
|
+
continue
|
|
1758
|
+
_WS_AVAILABLE = bool(_WS_ROOTS)
|
|
1759
|
+
if _WS_AVAILABLE:
|
|
1760
|
+
_log(f"workspaces: {[(w['name'], w['path'], w['allowRun']) for w in _WS_ROOTS]}")
|
|
1761
|
+
# Coding tasks are edit→test→fix loops — 5 steps is too tight. Bump the default
|
|
1762
|
+
# budget when workspaces are registered, unless the payload set one explicitly.
|
|
1763
|
+
if not cfg.get("maxSteps") and max_steps < 12:
|
|
1764
|
+
max_steps = 12
|
|
1765
|
+
_log("workspace registered → step budget raised to 12 (no explicit maxSteps)")
|
|
1766
|
+
|
|
1630
1767
|
def _rag_s3_and_loc():
|
|
1631
1768
|
client = _rag_s3_client(rag_region, rag_akid, rag_secret)
|
|
1632
1769
|
bucket, prefix = _rag_parse_location(rag_location)
|
|
@@ -1811,6 +1948,10 @@ def run_agent_chat(cfg):
|
|
|
1811
1948
|
_email_num = _tool_num
|
|
1812
1949
|
if _RAG_AVAILABLE:
|
|
1813
1950
|
_tool_num += 7 # download_file, unzip_file, list_dir, read_text_file, rag_index/add/search
|
|
1951
|
+
if _WS_AVAILABLE:
|
|
1952
|
+
_tool_num += 6 # grep_repo, read_file_lines, edit_lines, edit_file, create_file, run_command
|
|
1953
|
+
if not _RAG_AVAILABLE:
|
|
1954
|
+
_tool_num += 2 # list_dir + read_text_file registered for workspace browsing
|
|
1814
1955
|
_tool_count_word = {6: "SIX", 7: "SEVEN", 8: "EIGHT"}.get(_tool_num, str(_tool_num))
|
|
1815
1956
|
_rag_tool_block = (
|
|
1816
1957
|
" FILES & KNOWLEDGE BASE — to SUMMARIZE or ANSWER questions about a ZIP of files at a URL:\n"
|
|
@@ -1838,6 +1979,33 @@ def run_agent_chat(cfg):
|
|
|
1838
1979
|
"instantly — then list_dir/read_text_file or rag_index as needed. "
|
|
1839
1980
|
"If the user adds information, rag_add(source_url=url, text=...).\n"
|
|
1840
1981
|
) if _RAG_AVAILABLE else ""
|
|
1982
|
+
_ws_names = ", ".join(f"{w['name']} = {w['path']}" for w in _WS_ROOTS)
|
|
1983
|
+
_ws_tool_block = (
|
|
1984
|
+
f" WORKSPACES (local code folders you may READ and EDIT): {_ws_names}\n"
|
|
1985
|
+
" - list_dir(path) / read_text_file(path) — browse and read files.\n"
|
|
1986
|
+
" - grep_repo(pattern, path='', glob='') — search code, returns file:line hits. "
|
|
1987
|
+
"Use this to find the EXACT lines to change.\n"
|
|
1988
|
+
" - read_file_lines(path, start_line, end_line) — read WITH line numbers "
|
|
1989
|
+
"before editing.\n"
|
|
1990
|
+
" - edit_lines(path, start_line, end_line, new_content) — PRIMARY edit tool: "
|
|
1991
|
+
"replace a line range (backup saved automatically).\n"
|
|
1992
|
+
" - edit_file(path, old_string, new_string) — replace one exact unique string "
|
|
1993
|
+
"(must match byte-for-byte; prefer edit_lines).\n"
|
|
1994
|
+
" - create_file(path, content) — create a NEW file.\n"
|
|
1995
|
+
" - run_command(command, workdir='') — run a build/test command (npm test, mvn "
|
|
1996
|
+
"test, pytest…) to VERIFY changes (only in workspaces with run permission).\n"
|
|
1997
|
+
" NOTE: plain `import os`/`open()` is blocked — use ONLY these tools for files.\n"
|
|
1998
|
+
) if _WS_AVAILABLE else ""
|
|
1999
|
+
_ws_choose_line = (
|
|
2000
|
+
"- user asks to FIX A BUG / MODIFY / ADD CODE in a workspace -> "
|
|
2001
|
+
"grep_repo(pattern) (and/or rag_search if indexed) to FIND the file, THEN "
|
|
2002
|
+
"read_file_lines(file, around the hit) to see exact lines, THEN "
|
|
2003
|
+
"edit_lines(file, start, end, new_content) to change them, THEN "
|
|
2004
|
+
"run_command('…test…') to VERIFY if run permission exists — read failures and fix, "
|
|
2005
|
+
"iterate. Finish with final_answer summarizing WHAT you changed (files + lines). "
|
|
2006
|
+
"AFTER editing, re-read with grep_repo/read_file_lines (an S3 knowledge base may be "
|
|
2007
|
+
"stale — do not trust rag_search for freshly edited code).\n"
|
|
2008
|
+
) if _WS_AVAILABLE else ""
|
|
1841
2009
|
_pdf_read_tool_line = (
|
|
1842
2010
|
f" {_pdf_num}. extract_text_from_pdf(url) — READ/summarize an EXISTING PDF file "
|
|
1843
2011
|
"at a URL. This does NOT create a PDF (use create_pdf for that).\n"
|
|
@@ -1874,7 +2042,7 @@ def run_agent_chat(cfg):
|
|
|
1874
2042
|
"Use this ONLY when the task explicitly asks for the date or time.\n"
|
|
1875
2043
|
" 6. create_pdf(text, title='') — make a PDF document from text/data and return a "
|
|
1876
2044
|
"download link. Use ONLY when the task asks to create/make/generate/export a PDF.\n"
|
|
1877
|
-
+ _pdf_read_tool_line + _email_tool_line + _rag_tool_block +
|
|
2045
|
+
+ _pdf_read_tool_line + _email_tool_line + _rag_tool_block + _ws_tool_block +
|
|
1878
2046
|
"\n"
|
|
1879
2047
|
"http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
|
|
1880
2048
|
"\n"
|
|
@@ -1898,7 +2066,7 @@ def run_agent_chat(cfg):
|
|
|
1898
2066
|
"- a PDF of info you must LOOK UP first (e.g. 'get the world cup schedule then make a "
|
|
1899
2067
|
"pdf') -> FIRST web_search/fetch_url to gather the real facts, THEN in a later step "
|
|
1900
2068
|
"create_pdf(text=<the gathered facts>, title=...). Never PDF the request wording itself.\n"
|
|
1901
|
-
+ _pdf_read_choose_line + _email_choose_line + _rag_choose_line +
|
|
2069
|
+
+ _pdf_read_choose_line + _email_choose_line + _rag_choose_line + _ws_choose_line +
|
|
1902
2070
|
"- A specific known API/URL was given -> http_request().\n"
|
|
1903
2071
|
"\n"
|
|
1904
2072
|
"RULES:\n"
|
|
@@ -2608,11 +2776,40 @@ def run_agent_chat(cfg):
|
|
|
2608
2776
|
# Normalize code-block delimiters so smolagents' <code>…</code> parser
|
|
2609
2777
|
# matches even when the model wrote "<code >" / a markdown fence. Without
|
|
2610
2778
|
# this, tag-whitespace variants fail EVERY step → "Reached max steps".
|
|
2779
|
+
# GATED on real tool-call intent: a finished PROSE answer often contains
|
|
2780
|
+
# markdown fences (e.g. a ```java snippet) — blindly converting those to
|
|
2781
|
+
# <code> made smolagents EXECUTE the snippet as python → SyntaxError,
|
|
2782
|
+
# a wasted step, and a worse fallback answer (seen live). If there is no
|
|
2783
|
+
# executable intent at all and the prose reads like a final answer, wrap
|
|
2784
|
+
# it in final_answer() so the run finishes cleanly with the model's text.
|
|
2611
2785
|
if isinstance(content, str) and content:
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2786
|
+
has_code_tag = re.search(r"<code[\s>]", content, re.I) is not None
|
|
2787
|
+
fence_bodies = re.findall(r"```[a-zA-Z0-9_+-]*\n?([\s\S]*?)```", content)
|
|
2788
|
+
_tool_call_re = (
|
|
2789
|
+
r"\b(final_answer|download_file|unzip_file|list_dir|read_text_file|"
|
|
2790
|
+
r"rag_index|rag_add|rag_search|web_search|fetch_url|http_request|"
|
|
2791
|
+
r"calculate|get_current_datetime|create_pdf|extract_text_from_pdf|"
|
|
2792
|
+
r"send_email)\s*\("
|
|
2793
|
+
)
|
|
2794
|
+
fence_is_toolcall = any(re.search(_tool_call_re, b) for b in fence_bodies)
|
|
2795
|
+
if has_code_tag or fence_is_toolcall:
|
|
2796
|
+
normalized = _normalize_code_tags(content)
|
|
2797
|
+
if normalized != content:
|
|
2798
|
+
_log("normalized code-block tags (<code >/fence → <code>)")
|
|
2799
|
+
msg.content = normalized
|
|
2800
|
+
else:
|
|
2801
|
+
stripped = content.strip()
|
|
2802
|
+
looks_final = len(stripped) >= 400 or re.search(
|
|
2803
|
+
r"(^|\n)#{1,4}\s|\n\d+\.\s|\n[-*]\s|\|.+\|", stripped
|
|
2804
|
+
) is not None
|
|
2805
|
+
if stripped and looks_final:
|
|
2806
|
+
_log(
|
|
2807
|
+
f"prose-only turn ({len(stripped)} chars, no tool call) "
|
|
2808
|
+
"→ auto-wrapping as final_answer"
|
|
2809
|
+
)
|
|
2810
|
+
msg.content = (
|
|
2811
|
+
stripped + "\n<code>\nfinal_answer(" + repr(stripped) + ")\n</code>"
|
|
2812
|
+
)
|
|
2616
2813
|
except Exception as e: # noqa: BLE001
|
|
2617
2814
|
_log(f"content-normalize error: {e}")
|
|
2618
2815
|
return msg
|
|
@@ -2969,6 +3166,198 @@ def run_agent_chat(cfg):
|
|
|
2969
3166
|
except Exception as e: # noqa: BLE001
|
|
2970
3167
|
return f"Error: rag_search failed: {type(e).__name__}: {e}"
|
|
2971
3168
|
|
|
3169
|
+
@tool
|
|
3170
|
+
def grep_repo(pattern: str, path: str = "", glob: str = "") -> str:
|
|
3171
|
+
"""Search text files for a pattern (regex or literal) and return file:line hits. Use to find the EXACT lines to edit after rag_search located the relevant area.
|
|
3172
|
+
|
|
3173
|
+
Args:
|
|
3174
|
+
pattern: regex (or literal text) to search for.
|
|
3175
|
+
path: directory to search (a workspace or unzipped folder). Empty = first registered workspace.
|
|
3176
|
+
glob: optional filename filter, e.g. "*.java" or "*Controller*".
|
|
3177
|
+
"""
|
|
3178
|
+
try:
|
|
3179
|
+
import fnmatch
|
|
3180
|
+
import os
|
|
3181
|
+
root = _rag_read_allowed((path or "").strip() or (_WS_ROOTS[0]["path"] if _WS_ROOTS else "."))
|
|
3182
|
+
try:
|
|
3183
|
+
rx = re.compile(pattern)
|
|
3184
|
+
except re.error:
|
|
3185
|
+
rx = re.compile(re.escape(pattern))
|
|
3186
|
+
hits = []
|
|
3187
|
+
for abs_path, rel, _ext in _rag_iter_text_files(root):
|
|
3188
|
+
if glob and not fnmatch.fnmatch(os.path.basename(rel), glob) \
|
|
3189
|
+
and not fnmatch.fnmatch(rel, glob):
|
|
3190
|
+
continue
|
|
3191
|
+
try:
|
|
3192
|
+
with open(abs_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
3193
|
+
for i, line in enumerate(fh, 1):
|
|
3194
|
+
if rx.search(line):
|
|
3195
|
+
hits.append(f"{rel}:{i}: {line.rstrip()[:200]}")
|
|
3196
|
+
if len(hits) >= 100:
|
|
3197
|
+
break
|
|
3198
|
+
except OSError:
|
|
3199
|
+
continue
|
|
3200
|
+
if len(hits) >= 100:
|
|
3201
|
+
break
|
|
3202
|
+
_emit({"type": "step", "text": f"Searching code → {pattern[:60]} ({len(hits)} hits)"})
|
|
3203
|
+
if not hits:
|
|
3204
|
+
return f"No matches for {pattern!r} under {root}."
|
|
3205
|
+
more = "\n…(capped at 100 hits)" if len(hits) >= 100 else ""
|
|
3206
|
+
return f"Matches under {root}:\n" + "\n".join(hits) + more
|
|
3207
|
+
except Exception as e: # noqa: BLE001
|
|
3208
|
+
return f"Error: grep_repo failed: {type(e).__name__}: {e}"
|
|
3209
|
+
|
|
3210
|
+
@tool
|
|
3211
|
+
def read_file_lines(path: str, start_line: int = 1, end_line: int = 0) -> str:
|
|
3212
|
+
"""Read a file with LINE NUMBERS (for locating code before edit_lines). end_line 0 = to end (capped).
|
|
3213
|
+
|
|
3214
|
+
Args:
|
|
3215
|
+
path: local file path.
|
|
3216
|
+
start_line: first line to show (1-based).
|
|
3217
|
+
end_line: last line to show inclusive; 0 = rest of file (max 400 lines shown).
|
|
3218
|
+
"""
|
|
3219
|
+
try:
|
|
3220
|
+
rp = _rag_read_allowed((path or "").strip())
|
|
3221
|
+
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3222
|
+
lines = fh.readlines()
|
|
3223
|
+
s = max(1, int(start_line or 1))
|
|
3224
|
+
e = int(end_line or 0) or len(lines)
|
|
3225
|
+
e = min(e, len(lines), s + 399)
|
|
3226
|
+
body = "".join(f"{i}: {lines[i - 1]}" for i in range(s, e + 1))
|
|
3227
|
+
return f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}"
|
|
3228
|
+
except Exception as ex: # noqa: BLE001
|
|
3229
|
+
return f"Error: read_file_lines failed: {type(ex).__name__}: {ex}"
|
|
3230
|
+
|
|
3231
|
+
@tool
|
|
3232
|
+
def edit_lines(path: str, start_line: int, end_line: int, new_content: str) -> str:
|
|
3233
|
+
"""Replace lines start_line..end_line (inclusive, 1-based) of a workspace file with new_content. The PRIMARY edit tool — get exact line numbers from grep_repo/read_file_lines first. A backup is saved automatically.
|
|
3234
|
+
|
|
3235
|
+
Args:
|
|
3236
|
+
path: file inside a registered workspace.
|
|
3237
|
+
start_line: first line to replace (1-based).
|
|
3238
|
+
end_line: last line to replace (inclusive).
|
|
3239
|
+
new_content: replacement text (may be more or fewer lines).
|
|
3240
|
+
"""
|
|
3241
|
+
try:
|
|
3242
|
+
rp = _ws_write_allowed((path or "").strip())
|
|
3243
|
+
import os
|
|
3244
|
+
if not os.path.isfile(rp):
|
|
3245
|
+
return f"Error: not a file: {path}"
|
|
3246
|
+
backup = _ws_snapshot(rp)
|
|
3247
|
+
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3248
|
+
lines = fh.readlines()
|
|
3249
|
+
s, e = int(start_line), int(end_line)
|
|
3250
|
+
if not (1 <= s <= e <= len(lines)):
|
|
3251
|
+
return (f"Error: line range {s}-{e} is out of bounds "
|
|
3252
|
+
f"(file has {len(lines)} lines). Re-check with read_file_lines.")
|
|
3253
|
+
new_lines = new_content.splitlines(keepends=True)
|
|
3254
|
+
if new_lines and not new_lines[-1].endswith("\n"):
|
|
3255
|
+
new_lines[-1] += "\n"
|
|
3256
|
+
lines[s - 1:e] = new_lines
|
|
3257
|
+
with open(rp, "w", encoding="utf-8") as fh:
|
|
3258
|
+
fh.writelines(lines)
|
|
3259
|
+
_ws_record_change("edit", rp, backup)
|
|
3260
|
+
_emit({"type": "step", "text": f"Edited {os.path.basename(rp)} lines {s}-{e}"})
|
|
3261
|
+
shown = "".join(f"{i}: {lines[i - 1]}" for i in
|
|
3262
|
+
range(max(1, s - 2), min(len(lines), s + len(new_lines) + 1) + 1))
|
|
3263
|
+
return (f"Replaced lines {s}-{e} of {rp} with {len(new_lines)} line(s). "
|
|
3264
|
+
f"Backup saved. Updated region:\n{shown}")
|
|
3265
|
+
except Exception as ex: # noqa: BLE001
|
|
3266
|
+
return f"Error: edit_lines failed: {type(ex).__name__}: {ex}"
|
|
3267
|
+
|
|
3268
|
+
@tool
|
|
3269
|
+
def edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
3270
|
+
"""Replace an EXACT unique text occurrence in a workspace file (must match byte-for-byte incl. whitespace). Prefer edit_lines when you have line numbers. A backup is saved automatically.
|
|
3271
|
+
|
|
3272
|
+
Args:
|
|
3273
|
+
path: file inside a registered workspace.
|
|
3274
|
+
old_string: exact existing text to replace (must occur exactly once).
|
|
3275
|
+
new_string: replacement text.
|
|
3276
|
+
"""
|
|
3277
|
+
try:
|
|
3278
|
+
rp = _ws_write_allowed((path or "").strip())
|
|
3279
|
+
import os
|
|
3280
|
+
if not os.path.isfile(rp):
|
|
3281
|
+
return f"Error: not a file: {path}"
|
|
3282
|
+
with open(rp, "r", encoding="utf-8", errors="replace") as fh:
|
|
3283
|
+
text = fh.read()
|
|
3284
|
+
n = text.count(old_string)
|
|
3285
|
+
if n == 0:
|
|
3286
|
+
return ("Error: old_string not found — it must match EXACTLY "
|
|
3287
|
+
"(check whitespace with read_file_lines, or use edit_lines).")
|
|
3288
|
+
if n > 1:
|
|
3289
|
+
return f"Error: old_string occurs {n} times — add surrounding context to make it unique, or use edit_lines."
|
|
3290
|
+
backup = _ws_snapshot(rp)
|
|
3291
|
+
with open(rp, "w", encoding="utf-8") as fh:
|
|
3292
|
+
fh.write(text.replace(old_string, new_string, 1))
|
|
3293
|
+
_ws_record_change("edit", rp, backup)
|
|
3294
|
+
_emit({"type": "step", "text": f"Edited {os.path.basename(rp)}"})
|
|
3295
|
+
return f"Replaced 1 occurrence in {rp}. Backup saved."
|
|
3296
|
+
except Exception as ex: # noqa: BLE001
|
|
3297
|
+
return f"Error: edit_file failed: {type(ex).__name__}: {ex}"
|
|
3298
|
+
|
|
3299
|
+
@tool
|
|
3300
|
+
def create_file(path: str, content: str) -> str:
|
|
3301
|
+
"""Create a NEW file inside a registered workspace (errors if it already exists — use edit tools for existing files).
|
|
3302
|
+
|
|
3303
|
+
Args:
|
|
3304
|
+
path: new file path inside a workspace.
|
|
3305
|
+
content: full file content.
|
|
3306
|
+
"""
|
|
3307
|
+
try:
|
|
3308
|
+
rp = _ws_write_allowed((path or "").strip())
|
|
3309
|
+
import os
|
|
3310
|
+
if os.path.exists(rp):
|
|
3311
|
+
return f"Error: {path} already exists — use edit_lines/edit_file to change it."
|
|
3312
|
+
os.makedirs(os.path.dirname(rp) or ".", exist_ok=True)
|
|
3313
|
+
with open(rp, "w", encoding="utf-8") as fh:
|
|
3314
|
+
fh.write(content)
|
|
3315
|
+
_ws_record_change("create", rp, None)
|
|
3316
|
+
_emit({"type": "step", "text": f"Created {os.path.basename(rp)}"})
|
|
3317
|
+
return f"Created {rp} ({len(content)} chars)."
|
|
3318
|
+
except Exception as ex: # noqa: BLE001
|
|
3319
|
+
return f"Error: create_file failed: {type(ex).__name__}: {ex}"
|
|
3320
|
+
|
|
3321
|
+
@tool
|
|
3322
|
+
def run_command(command: str, workdir: str = "", timeout_seconds: int = 180) -> str:
|
|
3323
|
+
"""Run a build/test command inside a workspace that has run permission (e.g. 'npm test', 'mvn test', 'pytest'). Use to VERIFY code changes; read the output and fix failures.
|
|
3324
|
+
|
|
3325
|
+
Args:
|
|
3326
|
+
command: the command line (build/test runners only; no shells or sudo).
|
|
3327
|
+
workdir: directory to run in. Empty = first registered workspace.
|
|
3328
|
+
timeout_seconds: kill the command after this many seconds (max 600).
|
|
3329
|
+
"""
|
|
3330
|
+
try:
|
|
3331
|
+
import os
|
|
3332
|
+
import shlex
|
|
3333
|
+
import subprocess
|
|
3334
|
+
wd = _rag_read_allowed((workdir or "").strip() or (_WS_ROOTS[0]["path"] if _WS_ROOTS else "."))
|
|
3335
|
+
w = _ws_for_path(wd)
|
|
3336
|
+
if w is None or not w.get("allowRun"):
|
|
3337
|
+
return ("Error: running commands is not enabled for this workspace. "
|
|
3338
|
+
"Re-register it with: gonext-local-worker workspace add <path> --allow-run")
|
|
3339
|
+
argv = shlex.split(command or "")
|
|
3340
|
+
if not argv:
|
|
3341
|
+
return "Error: empty command."
|
|
3342
|
+
if argv[0] not in _WS_RUN_ALLOWED:
|
|
3343
|
+
return (f"Error: '{argv[0]}' is not an allowed runner. Allowed: "
|
|
3344
|
+
+ ", ".join(sorted(_WS_RUN_ALLOWED)))
|
|
3345
|
+
_emit({"type": "step", "text": f"Running → {command[:70]}"})
|
|
3346
|
+
t = max(5, min(int(timeout_seconds or 180), 600))
|
|
3347
|
+
# Scrubbed env: repo test scripts must never see worker secrets.
|
|
3348
|
+
proc = subprocess.run(
|
|
3349
|
+
argv, cwd=wd, env=_ws_scrubbed_env(), timeout=t,
|
|
3350
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
3351
|
+
)
|
|
3352
|
+
out = _ws_distill_output(proc.stdout or "")
|
|
3353
|
+
status = "PASSED (exit 0)" if proc.returncode == 0 else f"FAILED (exit {proc.returncode})"
|
|
3354
|
+
_emit({"type": "step", "text": f"Command {status.split()[0].lower()} → {command[:50]}"})
|
|
3355
|
+
return f"{status}\n{out}" if out.strip() else status
|
|
3356
|
+
except subprocess.TimeoutExpired:
|
|
3357
|
+
return f"Error: command timed out after {timeout_seconds}s."
|
|
3358
|
+
except Exception as ex: # noqa: BLE001
|
|
3359
|
+
return f"Error: run_command failed: {type(ex).__name__}: {ex}"
|
|
3360
|
+
|
|
2972
3361
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
2973
3362
|
get_current_datetime, create_pdf]
|
|
2974
3363
|
# Only register the PDF reader / email / RAG tools when available (kept in sync
|
|
@@ -2980,6 +3369,13 @@ def run_agent_chat(cfg):
|
|
|
2980
3369
|
if _RAG_AVAILABLE:
|
|
2981
3370
|
agent_tools += [download_file, unzip_file, list_dir, read_text_file,
|
|
2982
3371
|
rag_index, rag_add, rag_search]
|
|
3372
|
+
if _WS_AVAILABLE:
|
|
3373
|
+
agent_tools += [grep_repo, read_file_lines, edit_lines, edit_file,
|
|
3374
|
+
create_file, run_command]
|
|
3375
|
+
# list_dir/read_text_file are needed for workspace browsing even when RAG
|
|
3376
|
+
# (S3 indexing) isn't configured.
|
|
3377
|
+
if not _RAG_AVAILABLE:
|
|
3378
|
+
agent_tools += [list_dir, read_text_file]
|
|
2983
3379
|
agent_kwargs = dict(
|
|
2984
3380
|
tools=agent_tools,
|
|
2985
3381
|
model=model,
|
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.170",
|
|
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",
|