@tiens.nguyen/gonext-local-worker 1.0.174 → 1.0.177
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gonext-repl.mjs +107 -106
- package/package.json +1 -1
package/gonext-repl.mjs
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* It offers to register the CURRENT folder as an agent workspace (with a trust prompt
|
|
10
10
|
* that maps to --allow-run), authenticates with the worker key from ~/.gonext/worker.env
|
|
11
|
-
* (the same file `gonext-local-worker set` / the Wizard write),
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* (the same file `gonext-local-worker set` / the Wizard write), and loops on a `>> `
|
|
12
|
+
* prompt. Each question is ENQUEUED as a normal agent_chat job (models/budgets/RAG
|
|
13
|
+
* resolved from your user settings by POST /api/worker/agent-ask) and executed by the
|
|
14
|
+
* RUNNING gonext-local-worker daemon — so its terminal shows the full [gonext-agent]
|
|
15
|
+
* logs, exactly like a web question. The REPL polls the job and streams the persisted
|
|
16
|
+
* chunks live into the terminal.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
+
* Requires the worker daemon to be running (it claims the job).
|
|
19
|
+
* Commands: /exit /revert [runId] /help Ctrl-C stops following the current run.
|
|
18
20
|
*/
|
|
19
21
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
20
22
|
import { existsSync, statSync } from "node:fs";
|
|
@@ -34,6 +36,25 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
|
34
36
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
35
37
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
36
38
|
|
|
39
|
+
// ---------- flags ----------
|
|
40
|
+
const argv = process.argv.slice(2);
|
|
41
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
42
|
+
console.log(
|
|
43
|
+
"gonext — terminal agent REPL for the current folder\n\n" +
|
|
44
|
+
"Usage: gonext [-v|--verbose]\n\n" +
|
|
45
|
+
" -v, --verbose show the agent's diagnostic logs ([gonext-agent] …) and python stderr\n" +
|
|
46
|
+
" -h, --help this help\n\n" +
|
|
47
|
+
"Auth comes from ~/.gonext/worker.env; models/budgets/RAG come from your web Settings.\n" +
|
|
48
|
+
"Inside the REPL: /exit · /revert [runId] · /help — Ctrl-C stops following a turn.\n" +
|
|
49
|
+
"Questions are enqueued like web questions and executed by the RUNNING\n" +
|
|
50
|
+
"gonext-local-worker daemon — its terminal shows the full [gonext-agent] logs."
|
|
51
|
+
);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (argv.includes("-v") || argv.includes("--verbose")) {
|
|
55
|
+
process.env.GONEXT_REPL_VERBOSE = "1";
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
// ---------- auth: worker key + API base from ~/.gonext/worker.env ----------
|
|
38
59
|
dotenv.config({ path: ENV_FILE });
|
|
39
60
|
dotenv.config();
|
|
@@ -157,104 +178,86 @@ async function fetchAgentPayload(messages) {
|
|
|
157
178
|
return data;
|
|
158
179
|
}
|
|
159
180
|
|
|
160
|
-
// ---------- run one agent turn
|
|
161
|
-
|
|
181
|
+
// ---------- run one agent turn via the JOB QUEUE (the daemon executes it) ----------
|
|
182
|
+
// The question is enqueued exactly like a web question, so the RUNNING
|
|
183
|
+
// gonext-local-worker daemon claims + executes it — its terminal shows the full
|
|
184
|
+
// [gonext-agent] logs. We poll the job here and stream the persisted chunks live.
|
|
185
|
+
let following = false;
|
|
186
|
+
let followAborted = false;
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
// Map the job payload to the python cfg the same way the worker daemon does in
|
|
165
|
-
// runAgentChatJob (gonext-local-worker.mjs) — KEEP IN SYNC with that block.
|
|
166
|
-
const input = JSON.stringify({
|
|
167
|
-
messages: payload?.messages ?? [],
|
|
168
|
-
agentBaseURL: payload?.agentBaseURL ?? "",
|
|
169
|
-
agentApiKey: payload?.agentApiKey ?? "",
|
|
170
|
-
agentModelId: payload?.agentModelId ?? "",
|
|
171
|
-
codingBaseURL: payload?.codingBaseURL ?? "",
|
|
172
|
-
codingModelId: payload?.codingModelId ?? "",
|
|
173
|
-
tools: payload?.tools ?? ["http_request"],
|
|
174
|
-
maxSteps: payload?.maxSteps ?? 0,
|
|
175
|
-
researchBudget: payload?.researchBudget ?? 10,
|
|
176
|
-
agentToolMode: payload?.agentToolMode ?? "code",
|
|
177
|
-
apiBaseURL: apiBase,
|
|
178
|
-
workerKey,
|
|
179
|
-
emailEnabled: payload?.emailEnabled ?? false,
|
|
180
|
-
emailApiUrl: payload?.emailApiUrl ?? "",
|
|
181
|
-
emailApiMethod: payload?.emailApiMethod ?? "POST",
|
|
182
|
-
emailApiHeaders: payload?.emailApiHeaders ?? "",
|
|
183
|
-
emailBodyTemplate: payload?.emailBodyTemplate ?? "",
|
|
184
|
-
emailFrom: payload?.emailFrom ?? "",
|
|
185
|
-
emailAllowList: payload?.emailAllowList ?? "",
|
|
186
|
-
ragEnabled: payload?.ragEnabled ?? false,
|
|
187
|
-
ragS3Location: payload?.ragS3Location ?? "",
|
|
188
|
-
ragAwsRegion: payload?.ragAwsRegion ?? "",
|
|
189
|
-
ragAwsAccessKeyId: payload?.ragAwsAccessKeyId ?? "",
|
|
190
|
-
ragAwsSecretAccessKey: payload?.ragAwsSecretAccessKey ?? "",
|
|
191
|
-
ragEmbedModel: payload?.ragEmbedModel ?? "",
|
|
192
|
-
ragEmbedUrl: payload?.ragEmbedUrl ?? "",
|
|
193
|
-
ragTopK: payload?.ragTopK ?? 6,
|
|
194
|
-
workspaces: await readWorkspaces(),
|
|
195
|
-
});
|
|
188
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
196
189
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
190
|
+
/** Strip <think> markers for live display, and whole think blocks for the answer. */
|
|
191
|
+
const stripThinkTags = (s) => s.replace(/<\/?think>/gi, "");
|
|
192
|
+
const answerFrom = (s) =>
|
|
193
|
+
s
|
|
194
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
195
|
+
.replace(/<think>[\s\S]*$/i, "")
|
|
196
|
+
.trim();
|
|
197
|
+
|
|
198
|
+
async function runAgentTurn(history) {
|
|
199
|
+
const res = await fetch(`${apiBase}/api/worker/agent-ask`, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
|
|
202
|
+
body: JSON.stringify({ messages: history }),
|
|
203
|
+
});
|
|
204
|
+
const data = await res.json().catch(() => ({}));
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
if (res.status === 404) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"the API does not have POST /api/worker/agent-ask yet — deploy the latest API."
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
throw new Error(data?.error || `agent-ask failed (HTTP ${res.status})`);
|
|
209
212
|
}
|
|
213
|
+
const jobId = data?.jobId;
|
|
214
|
+
if (!jobId) throw new Error("agent-ask returned no jobId.");
|
|
210
215
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const line = buf.slice(0, nl);
|
|
224
|
-
buf = buf.slice(nl + 1);
|
|
225
|
-
if (!line.trim()) continue;
|
|
226
|
-
let ev;
|
|
227
|
-
try {
|
|
228
|
-
ev = JSON.parse(line);
|
|
229
|
-
} catch {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (ev.type === "step" && typeof ev.text === "string") {
|
|
233
|
-
process.stdout.write(dim(`\n· ${ev.text}`));
|
|
234
|
-
} else if (ev.type === "stream" && typeof ev.text === "string") {
|
|
235
|
-
process.stdout.write(dim(ev.text));
|
|
236
|
-
} else if (ev.type === "final" && typeof ev.text === "string") {
|
|
237
|
-
finalText = ev.text;
|
|
238
|
-
} else if (ev.type === "log" && process.env.GONEXT_REPL_VERBOSE === "1") {
|
|
239
|
-
process.stdout.write(dim(`\n[log] ${ev.text}`));
|
|
240
|
-
}
|
|
216
|
+
following = true;
|
|
217
|
+
followAborted = false;
|
|
218
|
+
const startedAt = Date.now();
|
|
219
|
+
let shownChars = 0;
|
|
220
|
+
let warnedPending = false;
|
|
221
|
+
try {
|
|
222
|
+
for (;;) {
|
|
223
|
+
if (followAborted) {
|
|
224
|
+
process.stdout.write(
|
|
225
|
+
dim("\n(stopped following — the job keeps running on the worker)\n")
|
|
226
|
+
);
|
|
227
|
+
return "";
|
|
241
228
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
229
|
+
const jr = await fetch(`${apiBase}/api/worker/jobs/${jobId}`, {
|
|
230
|
+
headers: { "X-Worker-Key": workerKey },
|
|
231
|
+
});
|
|
232
|
+
const job = await jr.json().catch(() => ({}));
|
|
233
|
+
if (!jr.ok) throw new Error(job?.error || `job poll failed (HTTP ${jr.status})`);
|
|
234
|
+
const text = typeof job.resultText === "string" ? job.resultText : "";
|
|
235
|
+
if (job.jobStatus === "completed") {
|
|
236
|
+
// Don't stream the final delta dim — the answer prints bright right after.
|
|
237
|
+
return answerFrom(text);
|
|
246
238
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
239
|
+
if (text.length > shownChars) {
|
|
240
|
+
process.stdout.write(dim(stripThinkTags(text.slice(shownChars))));
|
|
241
|
+
shownChars = text.length;
|
|
242
|
+
}
|
|
243
|
+
if (job.jobStatus === "failed" || job.jobStatus === "cancelled") {
|
|
244
|
+
throw new Error(job.errorMessage || `job ${job.jobStatus}`);
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
job.jobStatus === "pending" &&
|
|
248
|
+
!warnedPending &&
|
|
249
|
+
Date.now() - startedAt > 6000
|
|
250
|
+
) {
|
|
251
|
+
warnedPending = true;
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
red("\n⚠ no worker has claimed the job — is `gonext-local-worker` running?\n")
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
await sleep(700);
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
following = false;
|
|
260
|
+
}
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
// ---------- REPL ----------
|
|
@@ -280,10 +283,9 @@ async function main() {
|
|
|
280
283
|
|
|
281
284
|
const history = [];
|
|
282
285
|
rl.on("SIGINT", () => {
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
process.stdout.write(red("\n^C run aborted\n"));
|
|
286
|
+
if (following) {
|
|
287
|
+
followAborted = true;
|
|
288
|
+
process.stdout.write(red("\n^C "));
|
|
287
289
|
} else {
|
|
288
290
|
process.stdout.write(dim("\n(/exit to quit)\n") + cyan(">> "));
|
|
289
291
|
}
|
|
@@ -315,14 +317,13 @@ async function main() {
|
|
|
315
317
|
}
|
|
316
318
|
history.push({ role: "user", content: line });
|
|
317
319
|
try {
|
|
318
|
-
const
|
|
319
|
-
const answer = await runAgentTurn(payload);
|
|
320
|
+
const answer = await runAgentTurn(history);
|
|
320
321
|
if (answer) {
|
|
321
322
|
history.push({ role: "assistant", content: answer });
|
|
322
323
|
console.log(`\n\n${answer}\n`);
|
|
323
324
|
} else {
|
|
324
|
-
history.pop(); // failed turn — don't poison the next request's history
|
|
325
|
-
console.log(red("\n(no answer — run
|
|
325
|
+
history.pop(); // aborted/failed turn — don't poison the next request's history
|
|
326
|
+
console.log(red("\n(no answer — run aborted or failed)\n"));
|
|
326
327
|
}
|
|
327
328
|
} catch (err) {
|
|
328
329
|
history.pop();
|
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.177",
|
|
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",
|