@tiens.nguyen/gonext-local-worker 1.0.175 → 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 +91 -109
- 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";
|
|
@@ -43,9 +45,9 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
43
45
|
" -v, --verbose show the agent's diagnostic logs ([gonext-agent] …) and python stderr\n" +
|
|
44
46
|
" -h, --help this help\n\n" +
|
|
45
47
|
"Auth comes from ~/.gonext/worker.env; models/budgets/RAG come from your web Settings.\n" +
|
|
46
|
-
"Inside the REPL: /exit · /revert [runId] · /help — Ctrl-C
|
|
47
|
-
"
|
|
48
|
-
"daemon
|
|
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."
|
|
49
51
|
);
|
|
50
52
|
process.exit(0);
|
|
51
53
|
}
|
|
@@ -176,104 +178,86 @@ async function fetchAgentPayload(messages) {
|
|
|
176
178
|
return data;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
// ---------- run one agent turn
|
|
180
|
-
|
|
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;
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
codingModelId: payload?.codingModelId ?? "",
|
|
192
|
-
tools: payload?.tools ?? ["http_request"],
|
|
193
|
-
maxSteps: payload?.maxSteps ?? 0,
|
|
194
|
-
researchBudget: payload?.researchBudget ?? 10,
|
|
195
|
-
agentToolMode: payload?.agentToolMode ?? "code",
|
|
196
|
-
apiBaseURL: apiBase,
|
|
197
|
-
workerKey,
|
|
198
|
-
emailEnabled: payload?.emailEnabled ?? false,
|
|
199
|
-
emailApiUrl: payload?.emailApiUrl ?? "",
|
|
200
|
-
emailApiMethod: payload?.emailApiMethod ?? "POST",
|
|
201
|
-
emailApiHeaders: payload?.emailApiHeaders ?? "",
|
|
202
|
-
emailBodyTemplate: payload?.emailBodyTemplate ?? "",
|
|
203
|
-
emailFrom: payload?.emailFrom ?? "",
|
|
204
|
-
emailAllowList: payload?.emailAllowList ?? "",
|
|
205
|
-
ragEnabled: payload?.ragEnabled ?? false,
|
|
206
|
-
ragS3Location: payload?.ragS3Location ?? "",
|
|
207
|
-
ragAwsRegion: payload?.ragAwsRegion ?? "",
|
|
208
|
-
ragAwsAccessKeyId: payload?.ragAwsAccessKeyId ?? "",
|
|
209
|
-
ragAwsSecretAccessKey: payload?.ragAwsSecretAccessKey ?? "",
|
|
210
|
-
ragEmbedModel: payload?.ragEmbedModel ?? "",
|
|
211
|
-
ragEmbedUrl: payload?.ragEmbedUrl ?? "",
|
|
212
|
-
ragTopK: payload?.ragTopK ?? 6,
|
|
213
|
-
workspaces: await readWorkspaces(),
|
|
214
|
-
});
|
|
188
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
189
|
+
|
|
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();
|
|
215
197
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
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})`);
|
|
228
212
|
}
|
|
213
|
+
const jobId = data?.jobId;
|
|
214
|
+
if (!jobId) throw new Error("agent-ask returned no jobId.");
|
|
229
215
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const line = buf.slice(0, nl);
|
|
243
|
-
buf = buf.slice(nl + 1);
|
|
244
|
-
if (!line.trim()) continue;
|
|
245
|
-
let ev;
|
|
246
|
-
try {
|
|
247
|
-
ev = JSON.parse(line);
|
|
248
|
-
} catch {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (ev.type === "step" && typeof ev.text === "string") {
|
|
252
|
-
process.stdout.write(dim(`\n· ${ev.text}`));
|
|
253
|
-
} else if (ev.type === "stream" && typeof ev.text === "string") {
|
|
254
|
-
process.stdout.write(dim(ev.text));
|
|
255
|
-
} else if (ev.type === "final" && typeof ev.text === "string") {
|
|
256
|
-
finalText = ev.text;
|
|
257
|
-
} else if (ev.type === "log" && process.env.GONEXT_REPL_VERBOSE === "1") {
|
|
258
|
-
process.stdout.write(dim(`\n[log] ${ev.text}`));
|
|
259
|
-
}
|
|
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 "";
|
|
260
228
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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);
|
|
265
238
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
261
|
}
|
|
278
262
|
|
|
279
263
|
// ---------- REPL ----------
|
|
@@ -299,10 +283,9 @@ async function main() {
|
|
|
299
283
|
|
|
300
284
|
const history = [];
|
|
301
285
|
rl.on("SIGINT", () => {
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
process.stdout.write(red("\n^C run aborted\n"));
|
|
286
|
+
if (following) {
|
|
287
|
+
followAborted = true;
|
|
288
|
+
process.stdout.write(red("\n^C "));
|
|
306
289
|
} else {
|
|
307
290
|
process.stdout.write(dim("\n(/exit to quit)\n") + cyan(">> "));
|
|
308
291
|
}
|
|
@@ -334,14 +317,13 @@ async function main() {
|
|
|
334
317
|
}
|
|
335
318
|
history.push({ role: "user", content: line });
|
|
336
319
|
try {
|
|
337
|
-
const
|
|
338
|
-
const answer = await runAgentTurn(payload);
|
|
320
|
+
const answer = await runAgentTurn(history);
|
|
339
321
|
if (answer) {
|
|
340
322
|
history.push({ role: "assistant", content: answer });
|
|
341
323
|
console.log(`\n\n${answer}\n`);
|
|
342
324
|
} else {
|
|
343
|
-
history.pop(); // failed turn — don't poison the next request's history
|
|
344
|
-
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"));
|
|
345
327
|
}
|
|
346
328
|
} catch (err) {
|
|
347
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",
|