@tiens.nguyen/gonext-local-worker 1.0.252 → 1.0.254
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 +19 -8
- package/gonext-repl.mjs +27 -4
- package/gonext_agent_chat.py +61 -23
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1984,6 +1984,8 @@ async function runAgentChatJob(job) {
|
|
|
1984
1984
|
// each python "tokens" event, persisted to the job doc (Mongo) so the REPL can show
|
|
1985
1985
|
// it live and it survives on the completed job.
|
|
1986
1986
|
let latestCodeTokens = 0;
|
|
1987
|
+
// Task #84: peak single-request prompt-token count seen this turn (not the sum).
|
|
1988
|
+
let latestMaxCodeTokens = 0;
|
|
1987
1989
|
// Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
|
|
1988
1990
|
// prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
|
|
1989
1991
|
// markdown, which turns `#`-prefixed code comments into <h1> headings and
|
|
@@ -2111,16 +2113,24 @@ async function runAgentChatJob(job) {
|
|
|
2111
2113
|
enqueueText(event.text);
|
|
2112
2114
|
answerStreamed = true;
|
|
2113
2115
|
} else if (event.type === "tokens" && Number.isFinite(event.codeInput)) {
|
|
2114
|
-
// Prompt tokens sent to the agent CODE model (
|
|
2115
|
-
//
|
|
2116
|
-
// are seconds apart, so a fire-and-forget PATCH per event is cheap;
|
|
2117
|
-
// touches agentCodeTokens (never resultText/jobStatus),
|
|
2118
|
-
// chunk/completed writes.
|
|
2119
|
-
|
|
2120
|
-
|
|
2116
|
+
// Prompt tokens sent to the agent CODE model (tasks #83 accumulated + #84 peak).
|
|
2117
|
+
// Persist both to the job doc so they're in Mongo and the REPL can render them
|
|
2118
|
+
// live. Steps are seconds apart, so a fire-and-forget PATCH per event is cheap;
|
|
2119
|
+
// it only touches agentCodeTokens/agentMaxCodeTokens (never resultText/jobStatus),
|
|
2120
|
+
// so it can't race the chunk/completed writes.
|
|
2121
|
+
const nextTotal = Math.max(latestCodeTokens, event.codeInput);
|
|
2122
|
+
const nextMax = Number.isFinite(event.codeMax)
|
|
2123
|
+
? Math.max(latestMaxCodeTokens, event.codeMax)
|
|
2124
|
+
: latestMaxCodeTokens;
|
|
2125
|
+
if (nextTotal !== latestCodeTokens || nextMax !== latestMaxCodeTokens) {
|
|
2126
|
+
latestCodeTokens = nextTotal;
|
|
2127
|
+
latestMaxCodeTokens = nextMax;
|
|
2121
2128
|
workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
2122
2129
|
method: "PATCH",
|
|
2123
|
-
body: JSON.stringify({
|
|
2130
|
+
body: JSON.stringify({
|
|
2131
|
+
agentCodeTokens: latestCodeTokens,
|
|
2132
|
+
agentMaxCodeTokens: latestMaxCodeTokens,
|
|
2133
|
+
}),
|
|
2124
2134
|
}).catch(() => {});
|
|
2125
2135
|
}
|
|
2126
2136
|
} else if (event.type === "final" && typeof event.text === "string") {
|
|
@@ -2174,6 +2184,7 @@ async function runAgentChatJob(job) {
|
|
|
2174
2184
|
resultText: answerStreamed ? fullText : finalText || fullText,
|
|
2175
2185
|
tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
|
|
2176
2186
|
...(latestCodeTokens > 0 ? { agentCodeTokens: latestCodeTokens } : {}),
|
|
2187
|
+
...(latestMaxCodeTokens > 0 ? { agentMaxCodeTokens: latestMaxCodeTokens } : {}),
|
|
2177
2188
|
totalTimeSeconds,
|
|
2178
2189
|
}),
|
|
2179
2190
|
});
|
package/gonext-repl.mjs
CHANGED
|
@@ -122,6 +122,7 @@ const SPINNERS = [
|
|
|
122
122
|
// green, cyan, blue, indigo, pink, amber).
|
|
123
123
|
const WAIT_COLORS_256 = [120, 87, 111, 147, 218, 221];
|
|
124
124
|
const color256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
|
|
125
|
+
const orange = (s) => color256(208, s); // peak-token label (task #84)
|
|
125
126
|
// Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
|
|
126
127
|
const fmtTokens = (n) => {
|
|
127
128
|
if (n < 1000) return String(n);
|
|
@@ -564,6 +565,16 @@ async function loadSessionServer(cwd) {
|
|
|
564
565
|
}
|
|
565
566
|
}
|
|
566
567
|
|
|
568
|
+
// Peak single-request agent-code-model token count remembered per folder (task #84).
|
|
569
|
+
async function loadSessionMaxCodeTokens(cwd) {
|
|
570
|
+
try {
|
|
571
|
+
const raw = JSON.parse(await readFile(sessionFilePath(cwd), "utf8"));
|
|
572
|
+
return Number.isFinite(raw?.maxCodeTokens) ? raw.maxCodeTokens : 0;
|
|
573
|
+
} catch {
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
567
578
|
async function saveSession(cwd, history) {
|
|
568
579
|
try {
|
|
569
580
|
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
@@ -576,6 +587,7 @@ async function saveSession(cwd, history) {
|
|
|
576
587
|
updatedAt: new Date().toISOString(),
|
|
577
588
|
testAuto: sessionTestAuto,
|
|
578
589
|
deployServer: selectedServer, // remember the /server pick per folder (task #69)
|
|
590
|
+
maxCodeTokens: sessionMaxCodeTokens, // peak per-request tokens (task #84)
|
|
579
591
|
history: trimmed,
|
|
580
592
|
},
|
|
581
593
|
null,
|
|
@@ -919,6 +931,10 @@ let alwaysExtendOnMaxStep = false;
|
|
|
919
931
|
// Deploy target chosen via /server (task #69): {name, host, user} or null. Session-scoped;
|
|
920
932
|
// sent to /agent-ask as deployServer so the agent deploys to this host with KEY auth.
|
|
921
933
|
let selectedServer = null;
|
|
934
|
+
// Task #84: peak single-request agent-code-model token count for THIS workspace, kept
|
|
935
|
+
// as a max across turns and persisted per folder in the session file. Shown in orange
|
|
936
|
+
// next to the live token count.
|
|
937
|
+
let sessionMaxCodeTokens = 0;
|
|
922
938
|
|
|
923
939
|
// While a turn runs, mute readline's own echo/redraws so keystrokes can't corrupt the
|
|
924
940
|
// live status display and there's no way to type a new question — only Ctrl+C works.
|
|
@@ -1148,10 +1164,12 @@ async function runAgentTurn(history) {
|
|
|
1148
1164
|
// Line 2 (indented): the SPINNER + the playful word together, both in the cycling
|
|
1149
1165
|
// color — the spinner belongs with the word (dot on the label line, spinner leading
|
|
1150
1166
|
// the flavor line).
|
|
1151
|
-
// Task #83: after the playful word,
|
|
1152
|
-
// count (dim)
|
|
1153
|
-
//
|
|
1154
|
-
const tokLabel =
|
|
1167
|
+
// Task #83/#84: after the playful word, show the running agent-code-model token
|
|
1168
|
+
// count (dim) plus the per-workspace PEAK single-request count (orange), e.g.
|
|
1169
|
+
// " ⠙ Caffeinating… · 12.3k tok · 8.1k max". Both start at 0 and climb.
|
|
1170
|
+
const tokLabel =
|
|
1171
|
+
dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
|
|
1172
|
+
orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
|
|
1155
1173
|
process.stdout.write(
|
|
1156
1174
|
`${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
|
|
1157
1175
|
"\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`
|
|
@@ -1424,6 +1442,10 @@ async function runAgentTurn(history) {
|
|
|
1424
1442
|
if (Number.isFinite(job.agentCodeTokens) && job.agentCodeTokens > latestCodeTokens) {
|
|
1425
1443
|
latestCodeTokens = job.agentCodeTokens;
|
|
1426
1444
|
}
|
|
1445
|
+
// Peak single-request tokens (task #84): keep the per-workspace max across turns.
|
|
1446
|
+
if (Number.isFinite(job.agentMaxCodeTokens) && job.agentMaxCodeTokens > sessionMaxCodeTokens) {
|
|
1447
|
+
sessionMaxCodeTokens = job.agentMaxCodeTokens;
|
|
1448
|
+
}
|
|
1427
1449
|
const text = typeof job.resultText === "string" ? job.resultText : "";
|
|
1428
1450
|
if (job.jobStatus === "completed") {
|
|
1429
1451
|
// The job can flip to "completed" between polls with authoritative resultText that
|
|
@@ -1541,6 +1563,7 @@ async function main() {
|
|
|
1541
1563
|
// reflect them (both survive a fresh terminal in the same project).
|
|
1542
1564
|
sessionTestAuto = await loadSessionTestAuto(resolve(process.cwd()));
|
|
1543
1565
|
selectedServer = await loadSessionServer(resolve(process.cwd()));
|
|
1566
|
+
sessionMaxCodeTokens = await loadSessionMaxCodeTokens(resolve(process.cwd()));
|
|
1544
1567
|
// Show which models will answer, straight from user settings (also validates auth).
|
|
1545
1568
|
try {
|
|
1546
1569
|
const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
|
package/gonext_agent_chat.py
CHANGED
|
@@ -3117,11 +3117,17 @@ def run_agent_chat(cfg):
|
|
|
3117
3117
|
[_active_ws] if _active_ws else _WS_ROOTS
|
|
3118
3118
|
) if _WS_AVAILABLE else []
|
|
3119
3119
|
_ws_overview = _workspace_overview(_overview_roots) if _overview_roots else ""
|
|
3120
|
-
|
|
3120
|
+
# VARIABLE workspace context (task #86): the active-folder line, workspace list and
|
|
3121
|
+
# the on-disk folder overview change between turns (the agent creates/edits files),
|
|
3122
|
+
# so they must NOT sit inside the static tool_hint — they'd bust the prefix cache
|
|
3123
|
+
# mid-reference every turn. They join the variable tail of task_with_hint instead.
|
|
3124
|
+
_ws_context_block = (
|
|
3121
3125
|
_ws_current_line
|
|
3122
3126
|
+ f" Other registered workspaces (use only if the user names them): {_ws_names}\n"
|
|
3123
3127
|
f" Current folder contents:\n"
|
|
3124
3128
|
+ "\n".join(f" {line}" for line in _ws_overview.splitlines()) + "\n"
|
|
3129
|
+
) if _WS_AVAILABLE else ""
|
|
3130
|
+
_ws_tool_block = (
|
|
3125
3131
|
" - list_dir(path) / read_text_file(path) — browse and read files.\n"
|
|
3126
3132
|
" - grep_repo(pattern, path='', glob='') — search code, returns file:line hits. "
|
|
3127
3133
|
"Use this to find the EXACT lines to change.\n"
|
|
@@ -3225,19 +3231,18 @@ def run_agent_chat(cfg):
|
|
|
3225
3231
|
f"(Permission denied publickey,password), STOP and tell the user to run "
|
|
3226
3232
|
f"'ssh-copy-id {_du}@{_dh}' once, then retry. ***\n"
|
|
3227
3233
|
)
|
|
3234
|
+
# STATIC ONLY (task #86): everything in tool_hint must be byte-identical between
|
|
3235
|
+
# turns so the model server's prefix KV-cache covers it (see task_with_hint below).
|
|
3236
|
+
# Variable text — the deploy target and the current date/time — lives in the
|
|
3237
|
+
# variable tail next to the task instead.
|
|
3228
3238
|
tool_hint = (
|
|
3229
|
-
|
|
3230
|
-
# in a 20k-char hint, so the chosen server must lead, not trail (task #69 retest fix).
|
|
3231
|
-
_deploy_block
|
|
3232
|
-
+ f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
|
|
3239
|
+
f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
|
|
3233
3240
|
"brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
|
|
3234
3241
|
"tool's result (Observation) before the next step — use it to decide what to do "
|
|
3235
3242
|
"next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
|
|
3236
3243
|
"enough to answer, call final_answer(<your answer>) in a code block. Prefer "
|
|
3237
3244
|
"FEWER steps, and never repeat a tool call that already succeeded.\n\n"
|
|
3238
3245
|
"{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
|
|
3239
|
-
f"(Current date/time: {now_str} — pass a timezone to get_current_datetime() only "
|
|
3240
|
-
"if the task needs a DIFFERENT one.)\n"
|
|
3241
3246
|
+ _rag_tool_block + _ws_tool_block + _auto_test_block +
|
|
3242
3247
|
"\n"
|
|
3243
3248
|
"http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
|
|
@@ -3322,14 +3327,30 @@ def run_agent_chat(cfg):
|
|
|
3322
3327
|
f"Previous task: {_ck.get('task', '')}\n" + "\n".join(_kept)
|
|
3323
3328
|
)
|
|
3324
3329
|
|
|
3325
|
-
#
|
|
3326
|
-
#
|
|
3327
|
-
#
|
|
3330
|
+
# ORDER (task #86 — prefix-cache fix): STATIC first, VARIABLE last.
|
|
3331
|
+
# The ~16k-char tool reference is byte-identical between turns; leading with it
|
|
3332
|
+
# (right after the equally static system prompt) lets the model server's prefix
|
|
3333
|
+
# KV-cache cover ~7k tokens across turns. Previously the variable task text came
|
|
3334
|
+
# FIRST, so every new turn forced a cold re-eval of the whole reference (~2 min to
|
|
3335
|
+
# first token on step 1 of each turn; steps 2+ were 10-30s because within a turn
|
|
3336
|
+
# the prompt only grows at the end).
|
|
3337
|
+
# The TASK now comes LAST, under a loud header: recency is the position a model
|
|
3338
|
+
# attends to most, so anchoring is preserved (the historical "hint led with the
|
|
3339
|
+
# date → 3B model answered everything as a date question" regression was about a
|
|
3340
|
+
# misleading LEAD, not about where the task sits). The two VARIABLE blocks — the
|
|
3341
|
+
# current date/time and the deploy target — live here in the tail, next to the
|
|
3342
|
+
# task: the deploy target must stay near the task, not buried mid-hint (#69), and
|
|
3343
|
+
# the timestamp changes every minute, which would bust the cache mid-reference.
|
|
3328
3344
|
task_with_hint = (
|
|
3329
|
-
"
|
|
3330
|
-
|
|
3331
|
-
"-----
|
|
3332
|
-
|
|
3345
|
+
"----- TOOL REFERENCE (how to work — your actual TASK follows below) -----\n"
|
|
3346
|
+
+ tool_hint +
|
|
3347
|
+
"\n----- YOUR TASK -----\n"
|
|
3348
|
+
f"(Current date/time: {now_str} — pass a timezone to get_current_datetime() only "
|
|
3349
|
+
"if the task needs a DIFFERENT one.)\n"
|
|
3350
|
+
+ _deploy_block
|
|
3351
|
+
+ _ws_context_block +
|
|
3352
|
+
"\nTASK (answer THIS, choose the tool that fits it):\n"
|
|
3353
|
+
f"{task_text}\n"
|
|
3333
3354
|
)
|
|
3334
3355
|
|
|
3335
3356
|
# Track URLs that have already failed so we don't retry dead endpoints across steps.
|
|
@@ -3940,6 +3961,9 @@ def run_agent_chat(cfg):
|
|
|
3940
3961
|
# turn (task #83). Only this wrapper counts — the chat/brain helper calls
|
|
3941
3962
|
# (_chat_completion) don't route through here, so this is "code model only".
|
|
3942
3963
|
self._code_tokens_total = 0
|
|
3964
|
+
# Largest SINGLE-request prompt-token count seen this turn (task #84) — the
|
|
3965
|
+
# peak context size, not the sum. The REPL keeps a per-workspace max of this.
|
|
3966
|
+
self._code_tokens_max = 0
|
|
3943
3967
|
# stream_agent: request the completion with stream=True and reassemble the
|
|
3944
3968
|
# deltas into one message. Enabled for Ollama, where a long single-shot
|
|
3945
3969
|
# (non-stream) generation sends nothing until the very end — a reverse proxy
|
|
@@ -4182,6 +4206,19 @@ def run_agent_chat(cfg):
|
|
|
4182
4206
|
# _mark_first), read by the heartbeat below to switch its wording.
|
|
4183
4207
|
self._first_token_seen = False
|
|
4184
4208
|
|
|
4209
|
+
# Task #83: emit the PROMPT-token estimate NOW, at request time — so the
|
|
4210
|
+
# code-model token count climbs the instant a step is SENT, not only after a
|
|
4211
|
+
# slow step returns usage (a cold gemma4 can wait minutes for the first token,
|
|
4212
|
+
# during which the count would otherwise sit at 0). We reconcile to the exact
|
|
4213
|
+
# server-reported prompt_tokens after the call when it's higher.
|
|
4214
|
+
_step_est = self._estimate_prompt_tokens(args, kwargs)
|
|
4215
|
+
if _step_est > 0:
|
|
4216
|
+
self._code_tokens_total += _step_est
|
|
4217
|
+
if _step_est > self._code_tokens_max:
|
|
4218
|
+
self._code_tokens_max = _step_est
|
|
4219
|
+
_emit({"type": "tokens", "codeInput": self._code_tokens_total,
|
|
4220
|
+
"codeMax": self._code_tokens_max, "stepInput": _step_est})
|
|
4221
|
+
|
|
4185
4222
|
def _heartbeat():
|
|
4186
4223
|
waited = 0
|
|
4187
4224
|
while not hb_stop.wait(45):
|
|
@@ -4226,20 +4263,21 @@ def run_agent_chat(cfg):
|
|
|
4226
4263
|
raise last_err
|
|
4227
4264
|
finally:
|
|
4228
4265
|
hb_stop.set()
|
|
4229
|
-
# Task #83:
|
|
4230
|
-
#
|
|
4231
|
-
#
|
|
4266
|
+
# Task #83: reconcile the pre-call estimate with the server's EXACT
|
|
4267
|
+
# prompt_tokens when it's higher — keeps the running total monotonic (the
|
|
4268
|
+
# estimate already showed the count climbing at request time). Best-effort.
|
|
4232
4269
|
try:
|
|
4233
4270
|
tu = getattr(msg, "token_usage", None)
|
|
4234
|
-
|
|
4235
|
-
if
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4271
|
+
exact = int(getattr(tu, "input_tokens", 0) or 0) if tu else 0
|
|
4272
|
+
if exact > _step_est:
|
|
4273
|
+
self._code_tokens_total += (exact - _step_est)
|
|
4274
|
+
if exact > self._code_tokens_max:
|
|
4275
|
+
self._code_tokens_max = exact
|
|
4239
4276
|
_emit({
|
|
4240
4277
|
"type": "tokens",
|
|
4241
4278
|
"codeInput": self._code_tokens_total,
|
|
4242
|
-
"
|
|
4279
|
+
"codeMax": self._code_tokens_max,
|
|
4280
|
+
"stepInput": exact,
|
|
4243
4281
|
})
|
|
4244
4282
|
except Exception: # noqa: BLE001
|
|
4245
4283
|
pass
|
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.254",
|
|
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",
|