@tiens.nguyen/gonext-local-worker 1.0.228 → 1.0.230
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 +4 -0
- package/gonext-repl.mjs +196 -16
- package/gonext_agent_chat.py +59 -2
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1853,6 +1853,10 @@ async function runAgentChatJob(job) {
|
|
|
1853
1853
|
// REPL's "I can show a picker" signal (else python keeps hard-blocking, as on web).
|
|
1854
1854
|
jobId,
|
|
1855
1855
|
interactiveApproval: payload?.interactiveApproval === true,
|
|
1856
|
+
// Auto-test mode (/test-auto): the agent verifies its work and fixes until it passes.
|
|
1857
|
+
autoTest: payload?.autoTest === true,
|
|
1858
|
+
// Deploy target chosen via the terminal /server picker (task #69). Host/user only.
|
|
1859
|
+
deployServer: payload?.deployServer ?? null,
|
|
1856
1860
|
// Max web_search + fetch_url calls the agent may make per task (user-configurable
|
|
1857
1861
|
// in web Settings → Agent). Default 10; past it the retrieval tools refuse.
|
|
1858
1862
|
researchBudget: payload?.researchBudget ?? 10,
|
package/gonext-repl.mjs
CHANGED
|
@@ -163,6 +163,8 @@ if (!workerKey || !apiBase) {
|
|
|
163
163
|
// unknown-command guard, and /help, so they can never drift.
|
|
164
164
|
const COMMANDS = [
|
|
165
165
|
{ name: "/model", desc: "switch the coding model for this session" },
|
|
166
|
+
{ name: "/test-auto", desc: "toggle auto-test (verify & fix until it passes) for this folder" },
|
|
167
|
+
{ name: "/server", desc: "pick a deployment server (host/user) for this session" },
|
|
166
168
|
{ name: "/revert", usage: "/revert [runId]", desc: "undo the agent's file edits (latest run)" },
|
|
167
169
|
{ name: "/reset", aliases: ["/new"], desc: "forget this folder's saved conversation and start fresh" },
|
|
168
170
|
{ name: "/help", desc: "list these commands" },
|
|
@@ -503,6 +505,17 @@ async function loadSession(cwd) {
|
|
|
503
505
|
}
|
|
504
506
|
}
|
|
505
507
|
|
|
508
|
+
// The /test-auto choice is remembered per folder (stored alongside history in the same
|
|
509
|
+
// session file). Read it once at startup so the banner and agent-ask reflect it.
|
|
510
|
+
async function loadSessionTestAuto(cwd) {
|
|
511
|
+
try {
|
|
512
|
+
const raw = JSON.parse(await readFile(sessionFilePath(cwd), "utf8"));
|
|
513
|
+
return raw?.testAuto === true;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
506
519
|
async function saveSession(cwd, history) {
|
|
507
520
|
try {
|
|
508
521
|
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
@@ -510,7 +523,7 @@ async function saveSession(cwd, history) {
|
|
|
510
523
|
await writeFile(
|
|
511
524
|
sessionFilePath(cwd),
|
|
512
525
|
JSON.stringify(
|
|
513
|
-
{ cwd, updatedAt: new Date().toISOString(), history: trimmed },
|
|
526
|
+
{ cwd, updatedAt: new Date().toISOString(), testAuto: sessionTestAuto, history: trimmed },
|
|
514
527
|
null,
|
|
515
528
|
2
|
|
516
529
|
) + "\n"
|
|
@@ -580,6 +593,89 @@ async function chooseModel() {
|
|
|
580
593
|
console.log(green(` ✓ coding model → ${chosen}${chosen === def ? " (default)" : ""}\n`));
|
|
581
594
|
}
|
|
582
595
|
|
|
596
|
+
// Probe whether SSH KEY auth already works for a server (so we can tell the user to run
|
|
597
|
+
// ssh-copy-id if not). BatchMode=yes = never prompt for a password → exit!=0 if no key.
|
|
598
|
+
async function checkServerKeyAuth(s) {
|
|
599
|
+
try {
|
|
600
|
+
const ok = await new Promise((resolve) => {
|
|
601
|
+
const p = spawn(
|
|
602
|
+
"ssh",
|
|
603
|
+
[
|
|
604
|
+
"-o", "BatchMode=yes",
|
|
605
|
+
"-o", "ConnectTimeout=6",
|
|
606
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
607
|
+
`${s.user}@${s.host}`,
|
|
608
|
+
"true",
|
|
609
|
+
],
|
|
610
|
+
{ stdio: "ignore" }
|
|
611
|
+
);
|
|
612
|
+
p.on("exit", (code) => resolve(code === 0));
|
|
613
|
+
p.on("error", () => resolve(false));
|
|
614
|
+
});
|
|
615
|
+
if (ok) console.log(dim(" ✓ key auth works — deploys will connect without a password."));
|
|
616
|
+
else
|
|
617
|
+
console.log(
|
|
618
|
+
yellow(" key auth not set up yet.") +
|
|
619
|
+
dim(` Run once in your terminal: `) +
|
|
620
|
+
`ssh-copy-id ${s.user}@${s.host}`
|
|
621
|
+
);
|
|
622
|
+
} catch {
|
|
623
|
+
/* best-effort check — never block the picker */
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// /server — pick a deployment target (from the web app's Servers registry) for this
|
|
628
|
+
// session. Host/user only, never a secret; the agent deploys here with KEY auth (#69).
|
|
629
|
+
async function chooseServer() {
|
|
630
|
+
let servers;
|
|
631
|
+
try {
|
|
632
|
+
const res = await fetch(`${apiBase}/api/worker/deploy-servers`, {
|
|
633
|
+
headers: { "X-Worker-Key": workerKey },
|
|
634
|
+
});
|
|
635
|
+
const data = await res.json().catch(() => ({}));
|
|
636
|
+
if (!res.ok) {
|
|
637
|
+
console.log(red(` couldn't load servers: ${data?.error || `HTTP ${res.status}`}\n`));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
servers = Array.isArray(data?.servers) ? data.servers : [];
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.log(red(` couldn't load servers: ${err.message}\n`));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (servers.length === 0) {
|
|
646
|
+
console.log(dim(" No deployment servers yet — add them in the web app → Servers.\n"));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const activeHost = selectedServer?.host || "";
|
|
650
|
+
console.log(dim(" Choose a deployment server for this session:"));
|
|
651
|
+
servers.forEach((s, i) => {
|
|
652
|
+
const mark = s.host === activeHost ? green(" ●") : " ";
|
|
653
|
+
console.log(` ${mark} ${i + 1}. ${s.name} ${dim(`(${s.user}@${s.host})`)}`);
|
|
654
|
+
});
|
|
655
|
+
console.log(` 0. ${dim("none (clear selection)")}`);
|
|
656
|
+
const pick = (await ask(dim(" number (or Enter to keep current): "))).trim();
|
|
657
|
+
if (!pick) {
|
|
658
|
+
console.log("");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const n = Number.parseInt(pick, 10);
|
|
662
|
+
if (n === 0) {
|
|
663
|
+
selectedServer = null;
|
|
664
|
+
console.log(dim(" ✓ deployment server cleared.\n"));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const idx = n - 1;
|
|
668
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= servers.length) {
|
|
669
|
+
console.log(red(" invalid choice.\n"));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const s = servers[idx];
|
|
673
|
+
selectedServer = { name: s.name, host: s.host, user: s.user };
|
|
674
|
+
console.log(green(` ✓ deploy target → ${s.name} (${s.user}@${s.host})`));
|
|
675
|
+
await checkServerKeyAuth(selectedServer);
|
|
676
|
+
console.log("");
|
|
677
|
+
}
|
|
678
|
+
|
|
583
679
|
async function fetchAgentPayload(messages) {
|
|
584
680
|
const res = await fetch(`${apiBase}/api/worker/agent-payload`, {
|
|
585
681
|
method: "POST",
|
|
@@ -672,6 +768,13 @@ function approvalPrompt(command) {
|
|
|
672
768
|
// Per-session coding-model override chosen via /model (empty = use the account default).
|
|
673
769
|
// Sent to /agent-ask as codingModelOverride; the API validates it against the whitelist.
|
|
674
770
|
let sessionCodingModel = "";
|
|
771
|
+
// Auto-test mode (/test-auto): when on, the agent verifies its work (build/run/curl/…)
|
|
772
|
+
// and fixes → re-tests until it passes before finishing. Default off; remembered per
|
|
773
|
+
// folder in the session file; sent to /agent-ask as autoTest so python drives the loop.
|
|
774
|
+
let sessionTestAuto = false;
|
|
775
|
+
// Deploy target chosen via /server (task #69): {name, host, user} or null. Session-scoped;
|
|
776
|
+
// sent to /agent-ask as deployServer so the agent deploys to this host with KEY auth.
|
|
777
|
+
let selectedServer = null;
|
|
675
778
|
|
|
676
779
|
// While a turn runs, mute readline's own echo/redraws so keystrokes can't corrupt the
|
|
677
780
|
// live status display and there's no way to type a new question — only Ctrl+C works.
|
|
@@ -703,6 +806,65 @@ const answerFrom = (s) =>
|
|
|
703
806
|
.replace(/<think>[\s\S]*$/i, "")
|
|
704
807
|
.trim();
|
|
705
808
|
|
|
809
|
+
// --- Live-thought normalization (task "agent thought normalizing issue") ---------------
|
|
810
|
+
// The blinking status line names the model's current Thought. But the model often emits
|
|
811
|
+
// <code> with NO "Thought:" prose (or degenerates into a runaway), so the raw stream is
|
|
812
|
+
// a create_file(...) blob / escaped CSS — which used to render verbatim on the line
|
|
813
|
+
// ("◐ , #94a3b8);\n -webkit-background-clip…"). Fix: show a genuine thought ONLY when the
|
|
814
|
+
// model actually wrote prose; otherwise name the TOOL it's calling ("Writing App.js");
|
|
815
|
+
// otherwise show nothing (the line falls back to "Thinking…"). Never raw code/CSS.
|
|
816
|
+
const _baseName = (p) => (String(p || "").split(/[\\/]/).pop() || "").trim();
|
|
817
|
+
const THOUGHT_TOOL_LABELS = {
|
|
818
|
+
create_file: (a) => `Writing ${_baseName(a) || "a file"}`,
|
|
819
|
+
create_folder: (a) => `Creating ${_baseName(a) || "a folder"}`,
|
|
820
|
+
edit_lines: (a) => `Editing ${_baseName(a) || "a file"}`,
|
|
821
|
+
edit_file: (a) => `Editing ${_baseName(a) || "a file"}`,
|
|
822
|
+
read_file_lines: (a) => `Reading ${_baseName(a) || "a file"}`,
|
|
823
|
+
read_text_file: (a) => `Reading ${_baseName(a) || "a file"}`,
|
|
824
|
+
list_dir: (a) => `Listing ${_baseName(a) || "files"}`,
|
|
825
|
+
grep_repo: () => "Searching the code",
|
|
826
|
+
run_command: (a) => `Running ${a || "a command"}`,
|
|
827
|
+
stop_server: () => "Stopping the server",
|
|
828
|
+
deploy_web: () => "Deploying",
|
|
829
|
+
fetch_url: (a) => `Fetching ${_baseName(a) || "a page"}`,
|
|
830
|
+
web_search: () => "Searching the web",
|
|
831
|
+
http_request: () => "Calling an API",
|
|
832
|
+
create_pdf: () => "Building a PDF",
|
|
833
|
+
rag_search: () => "Searching the knowledge base",
|
|
834
|
+
rag_index: () => "Indexing the knowledge base",
|
|
835
|
+
download_file: () => "Downloading a file",
|
|
836
|
+
unzip_file: () => "Unzipping",
|
|
837
|
+
send_email: () => "Preparing an email",
|
|
838
|
+
open_url: (a) => `Opening ${a || "a link"}`,
|
|
839
|
+
final_answer: () => "Composing the answer",
|
|
840
|
+
};
|
|
841
|
+
// Does this candidate look like CODE / a data blob rather than a human thought? (Escaped
|
|
842
|
+
// newlines, braces, hex colors, -webkit-, a leading function call, or too few spaces.)
|
|
843
|
+
const _thoughtLooksCodey = (s) =>
|
|
844
|
+
/\\n|[{}]|=>|="|#[0-9a-fA-F]{3,6}\b|-webkit-|;\s*$|^\s*[<([]/.test(s) ||
|
|
845
|
+
/^\s*[a-z_]\w*\s*\(/i.test(s) ||
|
|
846
|
+
(s.length > 24 && (s.split(" ").length - 1) / s.length < 0.06);
|
|
847
|
+
// Raw in-fence stream → a short, meaningful status: a real Thought clause if the model
|
|
848
|
+
// wrote one, else a friendly label from the tool it's calling, else "" (→ "Thinking…").
|
|
849
|
+
const normalizeThought = (buf) => {
|
|
850
|
+
const t = String(buf || "");
|
|
851
|
+
const ci = t.search(/<code[\s>]/i);
|
|
852
|
+
const head = ci >= 0 ? t.slice(0, ci) : t;
|
|
853
|
+
// 1) a genuine Thought prose line (before the code) — only if it isn't itself code.
|
|
854
|
+
const prose = head
|
|
855
|
+
.replace(/^\s*Thought:\s*/i, "")
|
|
856
|
+
.split(/\n/)
|
|
857
|
+
.map((x) => x.trim())
|
|
858
|
+
.find(Boolean);
|
|
859
|
+
if (prose && !_thoughtLooksCodey(prose)) return prose.slice(0, 100);
|
|
860
|
+
// 2) otherwise name the tool being called (from the code part).
|
|
861
|
+
const codePart = ci >= 0 ? t.slice(ci) : t;
|
|
862
|
+
const m = /(?:<code>\s*)?\b([a-z_]\w*)\s*\(\s*(?:path\s*=\s*)?["']?([^"'\n,)]*)/i.exec(codePart);
|
|
863
|
+
if (m && THOUGHT_TOOL_LABELS[m[1]]) return THOUGHT_TOOL_LABELS[m[1]](m[2].trim()).slice(0, 100);
|
|
864
|
+
// 3) nothing human to show.
|
|
865
|
+
return "";
|
|
866
|
+
};
|
|
867
|
+
|
|
706
868
|
async function runAgentTurn(history) {
|
|
707
869
|
const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
|
|
708
870
|
method: "POST",
|
|
@@ -715,6 +877,10 @@ async function runAgentTurn(history) {
|
|
|
715
877
|
// The terminal can show a Yes/No picker → the agent PAUSES on a risky command and
|
|
716
878
|
// asks instead of hard-blocking it (interactive command-approval gate).
|
|
717
879
|
interactiveApproval: true,
|
|
880
|
+
// Auto-test mode (/test-auto): the agent verifies its work and fixes until it passes.
|
|
881
|
+
...(sessionTestAuto ? { autoTest: true } : {}),
|
|
882
|
+
// Deploy target chosen via /server — host/user only (no secret), for key-auth deploys.
|
|
883
|
+
...(selectedServer ? { deployServer: selectedServer } : {}),
|
|
718
884
|
...(sessionCodingModel ? { codingModelOverride: sessionCodingModel } : {}),
|
|
719
885
|
}),
|
|
720
886
|
});
|
|
@@ -774,15 +940,6 @@ async function runAgentTurn(history) {
|
|
|
774
940
|
// random word. `fenceThought` accumulates the raw in-fence text for the current step.
|
|
775
941
|
let liveThought = "";
|
|
776
942
|
let fenceThought = "";
|
|
777
|
-
// First non-empty Thought clause: drop the "Thought:" prefix, stop at the <code> block
|
|
778
|
-
// (we never show code), take the first line, and cap length so it never wraps.
|
|
779
|
-
const extractThought = (buf) => {
|
|
780
|
-
let t = String(buf || "");
|
|
781
|
-
const ci = t.search(/<code[\s>]/i);
|
|
782
|
-
if (ci >= 0) t = t.slice(0, ci);
|
|
783
|
-
t = t.replace(/^\s*Thought:\s*/i, "").split(/\n/)[0].trim();
|
|
784
|
-
return t;
|
|
785
|
-
};
|
|
786
943
|
const thinkSecs = () => Math.max(1, Math.round((Date.now() - thinkingSince) / 1000));
|
|
787
944
|
|
|
788
945
|
// The status is TWO in-place lines — the in-progress action, then the playful word
|
|
@@ -887,9 +1044,12 @@ async function runAgentTurn(history) {
|
|
|
887
1044
|
// Thought clause for the blinking line (task #64) — up to the <code> block, which is
|
|
888
1045
|
// where the prose ends and the tool call begins (we never surface code here).
|
|
889
1046
|
if (inStreamFence) {
|
|
890
|
-
|
|
1047
|
+
// Accumulate a bounded head of the stream (enough to see the Thought prose OR the
|
|
1048
|
+
// tool call), capped so a 16k-char runaway can't grow the buffer. normalizeThought
|
|
1049
|
+
// yields a clean label — never raw code/CSS.
|
|
1050
|
+
if (fenceThought.length < 400) {
|
|
891
1051
|
fenceThought += line + "\n";
|
|
892
|
-
const th =
|
|
1052
|
+
const th = normalizeThought(fenceThought);
|
|
893
1053
|
if (th) liveThought = th;
|
|
894
1054
|
}
|
|
895
1055
|
continue;
|
|
@@ -1025,8 +1185,8 @@ async function runAgentTurn(history) {
|
|
|
1025
1185
|
// no newline yet (a long reasoning sentence streams token-by-token) — recompute the
|
|
1026
1186
|
// live clause from the partial too, so the blinking line updates mid-sentence instead
|
|
1027
1187
|
// of only once the line finally breaks (task #64).
|
|
1028
|
-
if (inStreamFence &&
|
|
1029
|
-
const th =
|
|
1188
|
+
if (inStreamFence && fenceThought.length < 400) {
|
|
1189
|
+
const th = normalizeThought(fenceThought + carry);
|
|
1030
1190
|
if (th) liveThought = th;
|
|
1031
1191
|
}
|
|
1032
1192
|
// A non-empty remainder is a partial CONTENT line (heartbeats always arrive whole with
|
|
@@ -1161,6 +1321,8 @@ async function runAgentTurn(history) {
|
|
|
1161
1321
|
async function main() {
|
|
1162
1322
|
console.log(cyan("GoNext agent REPL") + dim(` · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
|
|
1163
1323
|
await ensureWorkspace();
|
|
1324
|
+
// Restore this folder's remembered auto-test choice so the banner + agent-ask reflect it.
|
|
1325
|
+
sessionTestAuto = await loadSessionTestAuto(resolve(process.cwd()));
|
|
1164
1326
|
// Show which models will answer, straight from user settings (also validates auth).
|
|
1165
1327
|
try {
|
|
1166
1328
|
const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
|
|
@@ -1169,8 +1331,9 @@ async function main() {
|
|
|
1169
1331
|
dim(
|
|
1170
1332
|
`agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
|
|
1171
1333
|
(p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
|
|
1172
|
-
(p.ragEnabled ? " · RAG on" : "")
|
|
1173
|
-
|
|
1334
|
+
(p.ragEnabled ? " · RAG on" : "") +
|
|
1335
|
+
` · auto-test: ${sessionTestAuto ? "on" : "off"}`
|
|
1336
|
+
) + (sessionTestAuto ? "" : dim(" (/test-auto to enable)"))
|
|
1174
1337
|
);
|
|
1175
1338
|
} catch (err) {
|
|
1176
1339
|
console.error(red(`gonext: ${err.message}`));
|
|
@@ -1250,6 +1413,23 @@ async function main() {
|
|
|
1250
1413
|
await chooseModel();
|
|
1251
1414
|
continue;
|
|
1252
1415
|
}
|
|
1416
|
+
if (line === "/server") {
|
|
1417
|
+
await chooseServer();
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (line === "/test-auto") {
|
|
1421
|
+
sessionTestAuto = !sessionTestAuto;
|
|
1422
|
+
await saveSession(cwd, history); // persist the choice for this folder
|
|
1423
|
+
console.log(
|
|
1424
|
+
(sessionTestAuto ? green("auto-test: ON") : dim("auto-test: OFF")) +
|
|
1425
|
+
dim(
|
|
1426
|
+
sessionTestAuto
|
|
1427
|
+
? " — the agent will verify its work and fix until it passes, for this folder.\n"
|
|
1428
|
+
: " for this folder.\n"
|
|
1429
|
+
)
|
|
1430
|
+
);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1253
1433
|
if (line === "/reset" || line === "/new") {
|
|
1254
1434
|
history.length = 0;
|
|
1255
1435
|
await clearSession(cwd);
|
package/gonext_agent_chat.py
CHANGED
|
@@ -2498,6 +2498,13 @@ def run_agent_chat(cfg):
|
|
|
2498
2498
|
# risky command and asks the user via the API instead of hard-blocking it.
|
|
2499
2499
|
_job_id = (cfg.get("jobId") or "").strip()
|
|
2500
2500
|
_interactive_approval = bool(cfg.get("interactiveApproval"))
|
|
2501
|
+
# Auto-test mode (/test-auto): after making changes the agent must VERIFY them and
|
|
2502
|
+
# fix → re-test until they pass before finishing. Drives a prompt directive + a
|
|
2503
|
+
# slightly higher step budget (the verify loop needs room). Terminal-only.
|
|
2504
|
+
_auto_test = bool(cfg.get("autoTest"))
|
|
2505
|
+
# Deploy target chosen via the terminal /server picker (task #69): {name, host, user}
|
|
2506
|
+
# or None. Host/user only — never a secret; deploys use SSH KEY auth to this host.
|
|
2507
|
+
_deploy_server = cfg.get("deployServer") if isinstance(cfg.get("deployServer"), dict) else None
|
|
2501
2508
|
# Optional dedicated coding/reasoning model for the CodeAgent's tool-use loop.
|
|
2502
2509
|
# Routing, plain replies and summarization stay on the chat model (better at
|
|
2503
2510
|
# natural language); the code model only drives http_request reasoning.
|
|
@@ -2731,6 +2738,12 @@ def run_agent_chat(cfg):
|
|
|
2731
2738
|
if not cfg.get("maxSteps") and max_steps < 12:
|
|
2732
2739
|
max_steps = 12
|
|
2733
2740
|
_log("workspace registered → step budget raised to 12 (no explicit maxSteps)")
|
|
2741
|
+
# Auto-test adds a verify → fix → re-test cycle on top of the work itself, so give
|
|
2742
|
+
# it more room (unless the user pinned a budget). Kept modest so a stuck test loop
|
|
2743
|
+
# still terminates rather than burning a huge budget.
|
|
2744
|
+
if _auto_test and not cfg.get("maxSteps") and max_steps < 16:
|
|
2745
|
+
max_steps = 16
|
|
2746
|
+
_log("auto-test on → step budget raised to 16 (no explicit maxSteps)")
|
|
2734
2747
|
|
|
2735
2748
|
def _rag_s3_and_loc():
|
|
2736
2749
|
client = _rag_s3_client(rag_region, rag_akid, rag_secret)
|
|
@@ -3098,8 +3111,52 @@ def run_agent_chat(cfg):
|
|
|
3098
3111
|
"- 'email …' / 'send an email to …' -> send_email(to, subject, body) "
|
|
3099
3112
|
"(previews first, then sends on confirm).\n"
|
|
3100
3113
|
) if _EMAIL_AVAILABLE else ""
|
|
3114
|
+
# Auto-test mode (/test-auto): a directive that makes the agent VERIFY its changes
|
|
3115
|
+
# and fix → re-test until they pass. Only meaningful for workspace code work; empty
|
|
3116
|
+
# otherwise so it costs no prompt-eval when off. Climbs the cheapest-sufficient
|
|
3117
|
+
# verification ladder and bakes in the guardrails (freeze the pass criterion, ignore
|
|
3118
|
+
# flakes, prove the test ran, cap retries) so it converges instead of chasing green.
|
|
3119
|
+
_auto_test_block = (
|
|
3120
|
+
"\nAUTO-TEST MODE (ON): after you CHANGE code you MUST verify it before "
|
|
3121
|
+
"final_answer — never finish on an untested change. Use the CHEAPEST check that "
|
|
3122
|
+
"proves the change, climbing only as needed:\n"
|
|
3123
|
+
" 1. grep_repo the edited file to confirm the change landed (and the old code is gone).\n"
|
|
3124
|
+
" 2. build / typecheck (e.g. run_command('npm run build') / 'tsc --noEmit' / 'go build').\n"
|
|
3125
|
+
" 3. run the project's tests (npm test / pytest / …) if they exist.\n"
|
|
3126
|
+
" 4. if it's a running app, start it and curl/fetch_url it, then check the response "
|
|
3127
|
+
"contains what the change should produce.\n"
|
|
3128
|
+
"DECIDE what 'passes' BEFORE you fix — do NOT weaken the check to make it green. If a "
|
|
3129
|
+
"check FAILS: read the error, make ONE fix, then RE-TEST the SAME way. If it passes only "
|
|
3130
|
+
"after re-running with no change, it was flaky — stop, don't keep editing. Give up after "
|
|
3131
|
+
"3 fix attempts on the same failure and final_answer honestly what still fails. Confirm a "
|
|
3132
|
+
"test actually exercised the change (e.g. the asserted text was really present), not an "
|
|
3133
|
+
"empty/no-op pass.\n"
|
|
3134
|
+
if (_auto_test and _WS_AVAILABLE) else ""
|
|
3135
|
+
)
|
|
3136
|
+
# Deploy target (/server): when the user picked one, tell the agent to deploy THERE
|
|
3137
|
+
# with key auth. Host/user only — the block never contains a password.
|
|
3138
|
+
_deploy_block = ""
|
|
3139
|
+
if _deploy_server and _deploy_server.get("host") and _deploy_server.get("user"):
|
|
3140
|
+
_dh = str(_deploy_server.get("host")).strip()
|
|
3141
|
+
_du = str(_deploy_server.get("user")).strip()
|
|
3142
|
+
_dn = str(_deploy_server.get("name") or _dh).strip()
|
|
3143
|
+
# Diagnostic: prove the picked server reached python (visible in the worker log).
|
|
3144
|
+
_log(f"deploy target: {_du}@{_dh} (server '{_dn}') — injecting into the prompt")
|
|
3145
|
+
_deploy_block = (
|
|
3146
|
+
f"\n*** DEPLOY TARGET — the user already chose a server: '{_dn}' → host={_dh}, "
|
|
3147
|
+
f"user={_du}. Do NOT search the project for host/user/credentials and do NOT ask "
|
|
3148
|
+
f"for them; you already have them. When the task is to DEPLOY, go straight to "
|
|
3149
|
+
f"deploy_web(local_dir, host='{_dh}', user='{_du}', remote_path='/var/www/<domain>', "
|
|
3150
|
+
f"domain='<domain>') — or ssh/scp/rsync to {_du}@{_dh}. KEY auth ONLY "
|
|
3151
|
+
f"(-o BatchMode=yes); NEVER ask for or type a password. If key auth isn't set up "
|
|
3152
|
+
f"(Permission denied publickey,password), STOP and tell the user to run "
|
|
3153
|
+
f"'ssh-copy-id {_du}@{_dh}' once, then retry. ***\n"
|
|
3154
|
+
)
|
|
3101
3155
|
tool_hint = (
|
|
3102
|
-
|
|
3156
|
+
# Deploy target FIRST (when a server is picked) — a small model buries context deep
|
|
3157
|
+
# in a 20k-char hint, so the chosen server must lead, not trail (task #69 retest fix).
|
|
3158
|
+
_deploy_block
|
|
3159
|
+
+ f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
|
|
3103
3160
|
"brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
|
|
3104
3161
|
"tool's result (Observation) before the next step — use it to decide what to do "
|
|
3105
3162
|
"next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
|
|
@@ -3108,7 +3165,7 @@ def run_agent_chat(cfg):
|
|
|
3108
3165
|
"{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
|
|
3109
3166
|
f"(Current date/time: {now_str} — pass a timezone to get_current_datetime() only "
|
|
3110
3167
|
"if the task needs a DIFFERENT one.)\n"
|
|
3111
|
-
+ _rag_tool_block + _ws_tool_block +
|
|
3168
|
+
+ _rag_tool_block + _ws_tool_block + _auto_test_block +
|
|
3112
3169
|
"\n"
|
|
3113
3170
|
"http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
|
|
3114
3171
|
"\n"
|
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.230",
|
|
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",
|