@tiens.nguyen/gonext-local-worker 1.0.149 → 1.0.150

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.
@@ -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.
@@ -1133,6 +1133,15 @@ def run_agent_chat(cfg):
1133
1133
  if max_steps < 1:
1134
1134
  max_steps = 8
1135
1135
 
1136
+ # Max web_search + fetch_url calls per task (user-configurable in web Settings →
1137
+ # Agent). Default 10; past it the retrieval tools refuse and steer to finish.
1138
+ try:
1139
+ research_budget = int(cfg.get("researchBudget") or 10)
1140
+ except (TypeError, ValueError):
1141
+ research_budget = 10
1142
+ if research_budget < 1:
1143
+ research_budget = 10
1144
+
1136
1145
  _log(
1137
1146
  f"start model={agent_model_id!r} base={agent_base_url!r} "
1138
1147
  f"codeModel={coding_model_id!r} codeBase={coding_base_url!r} maxSteps={max_steps}"
@@ -1440,9 +1449,9 @@ def run_agent_chat(cfg):
1440
1449
  "clean one often does not exist. After 1-2 searches/fetches, COMPILE what you have and "
1441
1450
  "finish: if a PDF was asked for, call create_pdf(text=<your compiled text>, title=...); "
1442
1451
  "otherwise call final_answer. NEVER repeat a web_search/fetch_url you already ran.\n"
1443
- "- RESEARCH BUDGET — you may make at most 3 web_search/fetch_url calls TOTAL per task "
1444
- "(rewording a query still counts). Reserve your remaining steps for create_pdf / "
1445
- "final_answer.\n"
1452
+ f"- RESEARCH BUDGET — you may make at most {research_budget} web_search/fetch_url calls "
1453
+ "TOTAL per task (rewording a query still counts). Reserve your remaining steps for "
1454
+ "create_pdf / final_answer.\n"
1446
1455
  "- Do NOT put final_answer outside the code block.\n\n"
1447
1456
  )
1448
1457
  # Lead with the TASK so the weak model anchors on what's actually being asked —
@@ -1486,10 +1495,10 @@ def run_agent_chat(cfg):
1486
1495
  # Hard research budget: at most N retrieval calls (web_search + fetch_url) per run.
1487
1496
  # Past it, retrieval tools refuse and steer to create_pdf/final_answer. Keeps steps
1488
1497
  # free to actually FINISH — the live failure mode was burning ALL steps on
1489
- # slightly-different searches and never producing the asked-for PDF. PDF tasks get a
1490
- # TIGHTER budget (2) so the model is steered to create_pdf a step sooner and has more
1491
- # steps left to actually emit that call itself (avoids leaning on the fallback).
1492
- _research = {"used": 0, "max": 2 if agent_pdf_requested else 3}
1498
+ # slightly-different searches and never producing the asked-for PDF. The ceiling is
1499
+ # user-configurable (web Settings Agent, default 10) and applies to both PDF and
1500
+ # plain tasks.
1501
+ _research = {"used": 0, "max": research_budget}
1493
1502
 
1494
1503
  def _research_spend(tool_name: str) -> str:
1495
1504
  """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.149",
3
+ "version": "1.0.150",
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",