@tiens.nguyen/gonext-local-worker 1.0.149 → 1.0.151
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 +45 -0
- package/gonext_agent_chat.py +32 -10
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -139,6 +139,10 @@ const OCR_TIMEOUT_MS = 180_000;
|
|
|
139
139
|
const OCR_DEBUG = String(process.env.GONEXT_OCR_DEBUG ?? "").trim() === "1";
|
|
140
140
|
const OCR_SKIP_CORRECTION = String(process.env.GONEXT_OCR_SKIP_CORRECTION ?? "").trim() === "1";
|
|
141
141
|
const OCR_CORRECT_TIMEOUT_MS = 120_000;
|
|
142
|
+
// Fire-and-forget warm-up of the agent's Ollama coding model. Generous because a
|
|
143
|
+
// cold load of a big/multimodal model into VRAM can take ~20s+; the request isn't
|
|
144
|
+
// awaited, so a long ceiling just avoids aborting an in-progress load.
|
|
145
|
+
const AGENT_WARMUP_TIMEOUT_MS = 180_000;
|
|
142
146
|
// OCR translation/correction is hardcoded to a managed Ollama service + model.
|
|
143
147
|
// (The user-configurable "OCR translation model" chooser was removed.)
|
|
144
148
|
const OCR_TRANSLATE_OLLAMA_URL = "https://ollama1.gomarsic.cc";
|
|
@@ -1432,9 +1436,47 @@ async function runTranscribeJob(job) {
|
|
|
1432
1436
|
}
|
|
1433
1437
|
}
|
|
1434
1438
|
|
|
1439
|
+
/**
|
|
1440
|
+
* Fire-and-forget pre-load of the agent's Ollama coding model. Ollama evicts idle
|
|
1441
|
+
* models (default OLLAMA_KEEP_ALIVE=5m), so the first agent step otherwise blocks
|
|
1442
|
+
* ~20s+ while llama-server spins up and pages the weights into VRAM. The agent
|
|
1443
|
+
* talks to Ollama over the OpenAI-compat /v1 endpoint, which IGNORES keep_alive —
|
|
1444
|
+
* so we hit the NATIVE /api/generate here (no prompt = load-only, returns as soon
|
|
1445
|
+
* as the model is resident) with keep_alive:-1 to trigger the load AND pin it in
|
|
1446
|
+
* memory. Kicked off the moment the job is claimed so the load overlaps python
|
|
1447
|
+
* startup + prompt assembly. Best-effort: a non-Ollama coding server just 404s and
|
|
1448
|
+
* the failure is swallowed — the job proceeds regardless.
|
|
1449
|
+
*/
|
|
1450
|
+
function warmUpOllamaCodingModel(codingBaseURL, codingModelId) {
|
|
1451
|
+
if (!codingBaseURL || !codingModelId) return;
|
|
1452
|
+
// codingBaseURL is normalized to …/v1 for OpenAI-compat; strip it to reach the
|
|
1453
|
+
// Ollama root where /api/generate lives.
|
|
1454
|
+
const root = String(codingBaseURL).replace(/\/+$/, "").replace(/\/v1$/i, "");
|
|
1455
|
+
const url = `${root}/api/generate`;
|
|
1456
|
+
fetch(url, {
|
|
1457
|
+
method: "POST",
|
|
1458
|
+
headers: { "Content-Type": "application/json" },
|
|
1459
|
+
body: JSON.stringify({ model: codingModelId, keep_alive: -1 }),
|
|
1460
|
+
signal: AbortSignal.timeout(AGENT_WARMUP_TIMEOUT_MS),
|
|
1461
|
+
})
|
|
1462
|
+
.then((res) => {
|
|
1463
|
+
console.log(
|
|
1464
|
+
`[gonext-worker] agent warm-up ${res.ok ? "ok (model loading/resident)" : `HTTP ${res.status}`} → ${url} model=${codingModelId}`
|
|
1465
|
+
);
|
|
1466
|
+
})
|
|
1467
|
+
.catch((err) => {
|
|
1468
|
+
const reason =
|
|
1469
|
+
err?.name === "TimeoutError" ? "still loading in background" : err?.message || "unreachable";
|
|
1470
|
+
console.log(`[gonext-worker] agent warm-up skipped (${reason}) → ${url}`);
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1435
1474
|
async function runAgentChatJob(job) {
|
|
1436
1475
|
const { jobId, payload } = job;
|
|
1437
1476
|
const start = Date.now();
|
|
1477
|
+
// Kick the Ollama coding model's cold load off immediately (non-blocking) so it
|
|
1478
|
+
// overlaps prompt assembly instead of stalling the first step. See helper above.
|
|
1479
|
+
warmUpOllamaCodingModel(payload?.codingBaseURL ?? "", payload?.codingModelId ?? "");
|
|
1438
1480
|
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
1439
1481
|
method: "PATCH",
|
|
1440
1482
|
body: JSON.stringify({ jobStatus: "running" }),
|
|
@@ -1507,6 +1549,9 @@ async function runAgentChatJob(job) {
|
|
|
1507
1549
|
codingModelId: payload?.codingModelId ?? "",
|
|
1508
1550
|
tools: payload?.tools ?? ["http_request"],
|
|
1509
1551
|
maxSteps: payload?.maxSteps ?? 5,
|
|
1552
|
+
// Max web_search + fetch_url calls the agent may make per task (user-configurable
|
|
1553
|
+
// in web Settings → Agent). Default 10; past it the retrieval tools refuse.
|
|
1554
|
+
researchBudget: payload?.researchBudget ?? 10,
|
|
1510
1555
|
// Tool-invocation mode for the agent: "code" (default, python CodeAgent) or
|
|
1511
1556
|
// "toolcall" (structured JSON tool calls). Passed through only; the API/web can
|
|
1512
1557
|
// set payload.agentToolMode to A/B — default keeps current behavior.
|
package/gonext_agent_chat.py
CHANGED
|
@@ -436,6 +436,19 @@ def _detect_model_id(base_url, api_key=""):
|
|
|
436
436
|
return ids[0]
|
|
437
437
|
|
|
438
438
|
|
|
439
|
+
def _clip(text: str, limit: int = 280) -> str:
|
|
440
|
+
"""Trim a one-line preview to ~limit chars WITHOUT cutting mid-word, adding an
|
|
441
|
+
ellipsis so it reads as intentionally shortened instead of broken. Collapses
|
|
442
|
+
internal whitespace/newlines so the panel shows a clean single line."""
|
|
443
|
+
text = " ".join(str(text).split())
|
|
444
|
+
if len(text) <= limit:
|
|
445
|
+
return text
|
|
446
|
+
cut = text[:limit].rsplit(" ", 1)[0].rstrip()
|
|
447
|
+
if not cut: # single very long token — hard cut as a fallback
|
|
448
|
+
cut = text[:limit].rstrip()
|
|
449
|
+
return f"{cut}…"
|
|
450
|
+
|
|
451
|
+
|
|
439
452
|
def _summarise_step(step_log):
|
|
440
453
|
"""Return a short human-readable description of an agent step."""
|
|
441
454
|
tool_calls = getattr(step_log, "tool_calls", None) or []
|
|
@@ -460,7 +473,7 @@ def _summarise_step(step_log):
|
|
|
460
473
|
if l.strip() and not l.strip().startswith("#")),
|
|
461
474
|
code[:80],
|
|
462
475
|
)
|
|
463
|
-
parts.append(first
|
|
476
|
+
parts.append(_clip(first, 120))
|
|
464
477
|
else:
|
|
465
478
|
if isinstance(args, dict):
|
|
466
479
|
method = args.get("method", "")
|
|
@@ -478,7 +491,7 @@ def _summarise_step(step_log):
|
|
|
478
491
|
# and show the first line of actual content.
|
|
479
492
|
lines = [l for l in obs.splitlines() if l.strip() and l.strip() != "Execution logs:"]
|
|
480
493
|
if lines:
|
|
481
|
-
parts.append(f"→ {lines[0]
|
|
494
|
+
parts.append(f"→ {_clip(lines[0])}")
|
|
482
495
|
|
|
483
496
|
if error:
|
|
484
497
|
err = str(error)
|
|
@@ -491,7 +504,7 @@ def _summarise_step(step_log):
|
|
|
491
504
|
# "Error: Reached max steps." made a successful run look failed (task #10).
|
|
492
505
|
parts.append("→ Wrapping up (reached step budget)…")
|
|
493
506
|
else:
|
|
494
|
-
parts.append(f"→ Error: {err
|
|
507
|
+
parts.append(f"→ Error: {_clip(err, 160)}")
|
|
495
508
|
|
|
496
509
|
# No numeric "Step N:" prefix — show only the semantic action.
|
|
497
510
|
return (" | ".join(parts) if parts else "thinking…")
|
|
@@ -1133,6 +1146,15 @@ def run_agent_chat(cfg):
|
|
|
1133
1146
|
if max_steps < 1:
|
|
1134
1147
|
max_steps = 8
|
|
1135
1148
|
|
|
1149
|
+
# Max web_search + fetch_url calls per task (user-configurable in web Settings →
|
|
1150
|
+
# Agent). Default 10; past it the retrieval tools refuse and steer to finish.
|
|
1151
|
+
try:
|
|
1152
|
+
research_budget = int(cfg.get("researchBudget") or 10)
|
|
1153
|
+
except (TypeError, ValueError):
|
|
1154
|
+
research_budget = 10
|
|
1155
|
+
if research_budget < 1:
|
|
1156
|
+
research_budget = 10
|
|
1157
|
+
|
|
1136
1158
|
_log(
|
|
1137
1159
|
f"start model={agent_model_id!r} base={agent_base_url!r} "
|
|
1138
1160
|
f"codeModel={coding_model_id!r} codeBase={coding_base_url!r} maxSteps={max_steps}"
|
|
@@ -1440,9 +1462,9 @@ def run_agent_chat(cfg):
|
|
|
1440
1462
|
"clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
|
|
1441
1463
|
"finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
|
|
1442
1464
|
"otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
|
|
1443
|
-
"- RESEARCH BUDGET — you may make at most
|
|
1444
|
-
"(rewording a query still counts). Reserve your remaining steps for
|
|
1445
|
-
"final_answer.\n"
|
|
1465
|
+
f"- RESEARCH BUDGET — you may make at most {research_budget} web_search/fetch_url calls "
|
|
1466
|
+
"TOTAL per task (rewording a query still counts). Reserve your remaining steps for "
|
|
1467
|
+
"create_pdf / final_answer.\n"
|
|
1446
1468
|
"- Do NOT put final_answer outside the code block.\n\n"
|
|
1447
1469
|
)
|
|
1448
1470
|
# Lead with the TASK so the weak model anchors on what's actually being asked —
|
|
@@ -1486,10 +1508,10 @@ def run_agent_chat(cfg):
|
|
|
1486
1508
|
# Hard research budget: at most N retrieval calls (web_search + fetch_url) per run.
|
|
1487
1509
|
# Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps steps
|
|
1488
1510
|
# free to actually FINISH — the live failure mode was burning ALL steps on
|
|
1489
|
-
# slightly-different searches and never producing the asked-for PDF.
|
|
1490
|
-
#
|
|
1491
|
-
#
|
|
1492
|
-
_research = {"used": 0, "max":
|
|
1511
|
+
# slightly-different searches and never producing the asked-for PDF. The ceiling is
|
|
1512
|
+
# user-configurable (web Settings → Agent, default 10) and applies to both PDF and
|
|
1513
|
+
# plain tasks.
|
|
1514
|
+
_research = {"used": 0, "max": research_budget}
|
|
1493
1515
|
|
|
1494
1516
|
def _research_spend(tool_name: str) -> str:
|
|
1495
1517
|
"""Return '' if under budget (and spend 1), else a refusal steering to finish."""
|
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.151",
|
|
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",
|