@tiens.nguyen/gonext-local-worker 1.0.249 → 1.0.250

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.
@@ -1980,6 +1980,10 @@ async function runAgentChatJob(job) {
1980
1980
  let inThink = false;
1981
1981
  let finalText = "";
1982
1982
  let answerStreamed = false; // an answer_stream delta already delivered the visible text
1983
+ // Task #83: running total of prompt tokens sent to the agent CODE model. Updated on
1984
+ // each python "tokens" event, persisted to the job doc (Mongo) so the REPL can show
1985
+ // it live and it survives on the completed job.
1986
+ let latestCodeTokens = 0;
1983
1987
  // Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
1984
1988
  // prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
1985
1989
  // markdown, which turns `#`-prefixed code comments into <h1> headings and
@@ -2106,6 +2110,19 @@ async function runAgentChatJob(job) {
2106
2110
  }
2107
2111
  enqueueText(event.text);
2108
2112
  answerStreamed = true;
2113
+ } else if (event.type === "tokens" && Number.isFinite(event.codeInput)) {
2114
+ // Prompt tokens sent to the agent CODE model (task #83). Persist the running
2115
+ // total to the job doc so it's in Mongo and the REPL can render it live. Steps
2116
+ // are seconds apart, so a fire-and-forget PATCH per event is cheap; it only
2117
+ // touches agentCodeTokens (never resultText/jobStatus), so it can't race the
2118
+ // chunk/completed writes.
2119
+ if (event.codeInput > latestCodeTokens) {
2120
+ latestCodeTokens = event.codeInput;
2121
+ workerFetch(`/api/worker/jobs/${jobId}`, {
2122
+ method: "PATCH",
2123
+ body: JSON.stringify({ agentCodeTokens: latestCodeTokens }),
2124
+ }).catch(() => {});
2125
+ }
2109
2126
  } else if (event.type === "final" && typeof event.text === "string") {
2110
2127
  closeStreamFence();
2111
2128
  if (inThink) {
@@ -2156,6 +2173,7 @@ async function runAgentChatJob(job) {
2156
2173
  // the whole story.
2157
2174
  resultText: answerStreamed ? fullText : finalText || fullText,
2158
2175
  tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
2176
+ ...(latestCodeTokens > 0 ? { agentCodeTokens: latestCodeTokens } : {}),
2159
2177
  totalTimeSeconds,
2160
2178
  }),
2161
2179
  });
package/gonext-repl.mjs CHANGED
@@ -122,6 +122,12 @@ 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
+ // Compact token count: 950 → "950", 12300 → "12.3k", 1500000 → "1.5M" (task #83).
126
+ const fmtTokens = (n) => {
127
+ if (n < 1000) return String(n);
128
+ if (n < 1_000_000) return `${(n / 1000).toFixed(n < 100_000 ? 1 : 0)}k`;
129
+ return `${(n / 1_000_000).toFixed(1)}M`;
130
+ };
125
131
  const BULLET = "●"; // a completed action
126
132
 
127
133
  // ---------- flags ----------
@@ -1065,6 +1071,9 @@ async function runAgentTurn(history) {
1065
1071
  // poll figure out how much of the FINAL answer is still unshown even when resultText
1066
1072
  // was replaced (not extended) on completion. See the completed-branch recovery below.
1067
1073
  let liveAnswer = "";
1074
+ // Running total of prompt tokens sent to the agent CODE model this turn (task #83),
1075
+ // read from the job doc each poll. Rendered on the thinking line after the playful word.
1076
+ let latestCodeTokens = 0;
1068
1077
  // First few KB of the raw stream as consumed — a cheap fingerprint to verify the
1069
1078
  // completed resultText really EXTENDS what we streamed (an old worker replaces it
1070
1079
  // with the shorter bare answer, which must not be sliced by shownChars).
@@ -1139,9 +1148,12 @@ async function runAgentTurn(history) {
1139
1148
  // Line 2 (indented): the SPINNER + the playful word together, both in the cycling
1140
1149
  // color — the spinner belongs with the word (dot on the label line, spinner leading
1141
1150
  // the flavor line).
1151
+ // Task #83: after the playful word, show the running agent-code-model token count
1152
+ // (dim), e.g. " ⠙ Caffeinating… · 12.3k tok". Only once we've seen some tokens.
1153
+ const tokLabel = latestCodeTokens > 0 ? dim(` · ${fmtTokens(latestCodeTokens)} tok`) : "";
1142
1154
  process.stdout.write(
1143
1155
  `${green(BULLET)} ${green(`${fit(primary)}… (${secs}s)`)}` +
1144
- "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}`
1156
+ "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`
1145
1157
  );
1146
1158
  statusShown = true;
1147
1159
  };
@@ -1407,6 +1419,10 @@ async function runAgentTurn(history) {
1407
1419
  continue;
1408
1420
  }
1409
1421
  jobRunning = job.jobStatus === "running";
1422
+ // Live agent-code-model token total (task #83) — monotonic, shown on the thinking line.
1423
+ if (Number.isFinite(job.agentCodeTokens) && job.agentCodeTokens > latestCodeTokens) {
1424
+ latestCodeTokens = job.agentCodeTokens;
1425
+ }
1410
1426
  const text = typeof job.resultText === "string" ? job.resultText : "";
1411
1427
  if (job.jobStatus === "completed") {
1412
1428
  // The job can flip to "completed" between polls with authoritative resultText that
@@ -3936,6 +3936,10 @@ def run_agent_chat(cfg):
3936
3936
  # literal messages array sent to /v1/chat/completions.
3937
3937
  class _LoggingModel(OpenAIServerModel):
3938
3938
  def __init__(self, *a, stream_agent=False, **kw):
3939
+ # Running total of PROMPT (input) tokens sent to the CODE model across the
3940
+ # turn (task #83). Only this wrapper counts — the chat/brain helper calls
3941
+ # (_chat_completion) don't route through here, so this is "code model only".
3942
+ self._code_tokens_total = 0
3939
3943
  # stream_agent: request the completion with stream=True and reassemble the
3940
3944
  # deltas into one message. Enabled for Ollama, where a long single-shot
3941
3945
  # (non-stream) generation sends nothing until the very end — a reverse proxy
@@ -4129,6 +4133,29 @@ def run_agent_chat(cfg):
4129
4133
  token_usage=TokenUsage(input_tokens=in_tok, output_tokens=out_tok),
4130
4134
  )
4131
4135
 
4136
+ def _estimate_prompt_tokens(self, args, kwargs):
4137
+ """Fallback token estimate (~4 chars/token) from the outgoing messages, used
4138
+ only when the model server doesn't report prompt usage (task #83)."""
4139
+ msgs = kwargs.get("messages")
4140
+ if msgs is None and args:
4141
+ msgs = args[0]
4142
+ if not msgs:
4143
+ return 0
4144
+ chars = 0
4145
+ try:
4146
+ for m in msgs:
4147
+ c = m.get("content") if isinstance(m, dict) else getattr(m, "content", None)
4148
+ if isinstance(c, list): # multimodal content parts
4149
+ for part in c:
4150
+ t = part.get("text") if isinstance(part, dict) else None
4151
+ if isinstance(t, str):
4152
+ chars += len(t)
4153
+ elif isinstance(c, str):
4154
+ chars += len(c)
4155
+ except Exception: # noqa: BLE001
4156
+ return 0
4157
+ return max(1, chars // 4)
4158
+
4132
4159
  def generate(self, *args, **kwargs):
4133
4160
  # Two safeguards on each step's reply BEFORE smolagents parses it for the
4134
4161
  # code block / final_answer:
@@ -4199,6 +4226,23 @@ def run_agent_chat(cfg):
4199
4226
  raise last_err
4200
4227
  finally:
4201
4228
  hb_stop.set()
4229
+ # Task #83: tally PROMPT tokens sent to the code model this step, then emit
4230
+ # the running total so the worker can persist it (Mongo) and the REPL can
4231
+ # show it live. Best-effort — never let accounting break the run.
4232
+ try:
4233
+ tu = getattr(msg, "token_usage", None)
4234
+ step_in = int(getattr(tu, "input_tokens", 0) or 0) if tu else 0
4235
+ if step_in <= 0:
4236
+ step_in = self._estimate_prompt_tokens(args, kwargs)
4237
+ if step_in > 0:
4238
+ self._code_tokens_total += step_in
4239
+ _emit({
4240
+ "type": "tokens",
4241
+ "codeInput": self._code_tokens_total,
4242
+ "stepInput": step_in,
4243
+ })
4244
+ except Exception: # noqa: BLE001
4245
+ pass
4202
4246
  try:
4203
4247
  content = getattr(msg, "content", None)
4204
4248
  if content is None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.249",
3
+ "version": "1.0.250",
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",