@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.
@@ -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.
@@ -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[:120])
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][:200]}")
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[:120]}")
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 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"
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. 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}
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.149",
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",