@tiens.nguyen/gonext-local-worker 1.0.145 → 1.0.148
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 +31 -0
- package/gonext_agent_chat.py +92 -36
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1538,6 +1538,19 @@ async function runAgentChatJob(job) {
|
|
|
1538
1538
|
|
|
1539
1539
|
let inThink = false;
|
|
1540
1540
|
let finalText = "";
|
|
1541
|
+
// Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
|
|
1542
|
+
// prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
|
|
1543
|
+
// markdown, which turns `#`-prefixed code comments into <h1> headings and
|
|
1544
|
+
// swallows the tag fragments. Wrap only the raw stream in a fenced code block so
|
|
1545
|
+
// it renders verbatim; `step` labels stay outside the fence as normal prose.
|
|
1546
|
+
// Tilde fences (~~~) avoid colliding with any ``` the model itself emits.
|
|
1547
|
+
let inStreamFence = false;
|
|
1548
|
+
const closeStreamFence = () => {
|
|
1549
|
+
if (inStreamFence) {
|
|
1550
|
+
inStreamFence = false;
|
|
1551
|
+
enqueueText("\n~~~\n\n");
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1541
1554
|
|
|
1542
1555
|
// The create_pdf tool uses WeasyPrint, whose Homebrew libs (pango/cairo/gobject)
|
|
1543
1556
|
// live in /opt/homebrew/lib (Apple Silicon) or /usr/local/lib (Intel). macOS doesn't
|
|
@@ -1554,13 +1567,30 @@ async function runAgentChatJob(job) {
|
|
|
1554
1567
|
await runProcessWithStreamingStdout(python, [scriptPath], input, timeoutMs, (event) => {
|
|
1555
1568
|
if (event.type === "log" && typeof event.text === "string") {
|
|
1556
1569
|
console.log(`[gonext-agent] ${event.text}`);
|
|
1570
|
+
} else if (event.type === "stream" && typeof event.text === "string") {
|
|
1571
|
+
// Live model token stream (the agent's Thought / reasoning as it generates).
|
|
1572
|
+
// Goes inside the same <think> block as the step summaries, but RAW — no
|
|
1573
|
+
// "\n\n" separator, so the tokens concatenate into flowing text. Wrapped in
|
|
1574
|
+
// a code fence (opened lazily on the first stream token) so the web viewer
|
|
1575
|
+
// renders it verbatim instead of as markdown.
|
|
1576
|
+
if (!inThink) {
|
|
1577
|
+
inThink = true;
|
|
1578
|
+
enqueueText("<think>");
|
|
1579
|
+
}
|
|
1580
|
+
if (!inStreamFence) {
|
|
1581
|
+
inStreamFence = true;
|
|
1582
|
+
enqueueText("~~~\n");
|
|
1583
|
+
}
|
|
1584
|
+
enqueueText(event.text);
|
|
1557
1585
|
} else if (event.type === "step" && typeof event.text === "string") {
|
|
1586
|
+
closeStreamFence();
|
|
1558
1587
|
if (!inThink) {
|
|
1559
1588
|
inThink = true;
|
|
1560
1589
|
enqueueText("<think>");
|
|
1561
1590
|
}
|
|
1562
1591
|
enqueueText(event.text + "\n\n");
|
|
1563
1592
|
} else if (event.type === "final" && typeof event.text === "string") {
|
|
1593
|
+
closeStreamFence();
|
|
1564
1594
|
if (inThink) {
|
|
1565
1595
|
inThink = false;
|
|
1566
1596
|
enqueueText("</think>");
|
|
@@ -1570,6 +1600,7 @@ async function runAgentChatJob(job) {
|
|
|
1570
1600
|
}
|
|
1571
1601
|
}, pdfEnv);
|
|
1572
1602
|
|
|
1603
|
+
closeStreamFence();
|
|
1573
1604
|
if (inThink) {
|
|
1574
1605
|
enqueueText("</think>");
|
|
1575
1606
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -15,9 +15,11 @@ Reads on stdin:
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
Emits NDJSON lines on stdout:
|
|
18
|
-
{"type": "log",
|
|
19
|
-
{"type": "step",
|
|
20
|
-
{"type": "
|
|
18
|
+
{"type": "log", "text": "..."} — worker logs to console, not shown in chat
|
|
19
|
+
{"type": "step", "text": "..."} — a summary line shown in the <think> area
|
|
20
|
+
{"type": "stream", "text": "..."} — RAW model token(s) streamed live into <think>
|
|
21
|
+
(the agent's Thought/reasoning as it generates)
|
|
22
|
+
{"type": "final", "text": "..."} — assistant answer
|
|
21
23
|
"""
|
|
22
24
|
import contextlib
|
|
23
25
|
import json
|
|
@@ -1892,33 +1894,34 @@ def run_agent_chat(cfg):
|
|
|
1892
1894
|
_log(f"streamed generate failed ({e}) → falling back to non-streaming")
|
|
1893
1895
|
return super().generate(*args, **kwargs)
|
|
1894
1896
|
|
|
1895
|
-
def
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
completion_kwargs = self._prepare_completion_kwargs(
|
|
1901
|
-
messages=messages,
|
|
1902
|
-
stop_sequences=stop_sequences,
|
|
1903
|
-
response_format=response_format,
|
|
1904
|
-
tools_to_call_from=tools_to_call_from,
|
|
1905
|
-
model=self.model_id,
|
|
1906
|
-
custom_role_conversions=self.custom_role_conversions,
|
|
1907
|
-
convert_images_to_image_urls=True,
|
|
1908
|
-
**kwargs,
|
|
1909
|
-
)
|
|
1910
|
-
completion_kwargs["stream"] = True
|
|
1911
|
-
# Ask for a usage summary in the final chunk (OpenAI streaming convention;
|
|
1912
|
-
# Ollama honors it). Harmless if the server ignores it — usage stays 0.
|
|
1913
|
-
completion_kwargs["stream_options"] = {"include_usage": True}
|
|
1897
|
+
def _run_stream(self, completion_kwargs):
|
|
1898
|
+
"""Issue ONE streaming completion, stream tokens live to the UI, and return
|
|
1899
|
+
(content, role, in_tok, out_tok, reasoning_len). `content` is ONLY the visible
|
|
1900
|
+
message text (for smolagents' parser); reasoning-channel tokens are streamed to
|
|
1901
|
+
the Thinking panel but never accumulated into content."""
|
|
1914
1902
|
self._apply_rate_limit()
|
|
1915
1903
|
t0 = time.monotonic()
|
|
1916
1904
|
_log("streamed generate: request issued (stream=True), awaiting first token…")
|
|
1917
1905
|
stream = self.client.chat.completions.create(**completion_kwargs)
|
|
1918
1906
|
parts = []
|
|
1919
1907
|
role = "assistant"
|
|
1920
|
-
in_tok = out_tok = 0
|
|
1908
|
+
in_tok = out_tok = reasoning_len = 0
|
|
1921
1909
|
first_token_at = None
|
|
1910
|
+
|
|
1911
|
+
def _mark_first():
|
|
1912
|
+
# First streamed byte (content OR reasoning). The gap t0→here is pure
|
|
1913
|
+
# prompt-eval time (server sent nothing before this); everything after is
|
|
1914
|
+
# token generation with bytes flowing, so the proxy idle timer resets. If
|
|
1915
|
+
# this never logs, the request died in prompt-eval BEFORE any token —
|
|
1916
|
+
# streaming can't help there (shrink the model / warm the prompt cache).
|
|
1917
|
+
nonlocal first_token_at
|
|
1918
|
+
if first_token_at is None:
|
|
1919
|
+
first_token_at = time.monotonic()
|
|
1920
|
+
_log(f"streamed generate: FIRST token after {first_token_at - t0:.1f}s "
|
|
1921
|
+
"(prompt-eval done; tokens now flowing)")
|
|
1922
|
+
# Separate this step's live thinking from the previous step summary.
|
|
1923
|
+
_emit({"type": "stream", "text": "\n"})
|
|
1924
|
+
|
|
1922
1925
|
for chunk in stream:
|
|
1923
1926
|
usage = getattr(chunk, "usage", None)
|
|
1924
1927
|
if usage is not None:
|
|
@@ -1932,26 +1935,79 @@ def run_agent_chat(cfg):
|
|
|
1932
1935
|
continue
|
|
1933
1936
|
if getattr(delta, "role", None):
|
|
1934
1937
|
role = delta.role
|
|
1938
|
+
# Live "thinking": stream BOTH the visible content AND any reasoning-channel
|
|
1939
|
+
# tokens (gemma4/deepseek-class models emit their chain-of-thought there,
|
|
1940
|
+
# leaving `content` empty) to the web Thinking panel as they arrive. Only
|
|
1941
|
+
# `content` is accumulated into the message smolagents parses — reasoning is
|
|
1942
|
+
# display-only and must NEVER reach the <code> parser.
|
|
1943
|
+
rpiece = (getattr(delta, "reasoning", None)
|
|
1944
|
+
or getattr(delta, "reasoning_content", None))
|
|
1945
|
+
if rpiece:
|
|
1946
|
+
_mark_first()
|
|
1947
|
+
reasoning_len += len(rpiece)
|
|
1948
|
+
_emit({"type": "stream", "text": rpiece})
|
|
1935
1949
|
piece = getattr(delta, "content", None)
|
|
1936
1950
|
if piece:
|
|
1937
|
-
|
|
1938
|
-
# First streamed byte. The gap t0→here is pure prompt-eval time
|
|
1939
|
-
# (server sent nothing before this); everything after is token
|
|
1940
|
-
# generation with bytes flowing, so the proxy idle timer resets.
|
|
1941
|
-
# If this line never appears in the log, the request died in
|
|
1942
|
-
# prompt-eval BEFORE any token — streaming can't help there
|
|
1943
|
-
# (shrink the model / warm the prompt cache instead).
|
|
1944
|
-
first_token_at = time.monotonic()
|
|
1945
|
-
_log(f"streamed generate: FIRST token after {first_token_at - t0:.1f}s "
|
|
1946
|
-
"(prompt-eval done; tokens now flowing)")
|
|
1947
|
-
_emit({"type": "step", "text": "Model is responding…"})
|
|
1951
|
+
_mark_first()
|
|
1948
1952
|
parts.append(piece)
|
|
1953
|
+
_emit({"type": "stream", "text": piece})
|
|
1949
1954
|
content = "".join(parts)
|
|
1955
|
+
_log(f"streamed generate assembled {len(content)} chars "
|
|
1956
|
+
f"(in={in_tok} out={out_tok} tokens, reasoning={reasoning_len} chars)")
|
|
1957
|
+
return content, role, in_tok, out_tok, reasoning_len
|
|
1958
|
+
|
|
1959
|
+
# Injected when a turn produced ONLY reasoning and no message content — a hard,
|
|
1960
|
+
# model-agnostic steer to emit the actionable code block instead of more analysis.
|
|
1961
|
+
_CODE_NOW_DIRECTIVE = (
|
|
1962
|
+
"You wrote analysis but produced NO code block, so nothing ran. Output ONLY a "
|
|
1963
|
+
"single code block NOW that calls exactly one tool and nothing else, e.g.:\n"
|
|
1964
|
+
"<code>\nweb_search(query=\"...\")\n</code>\n"
|
|
1965
|
+
"No explanation, no <thought>, no <think> — just the <code>…</code> block. "
|
|
1966
|
+
"If you already have enough information, call final_answer(...) or create_pdf(...)."
|
|
1967
|
+
)
|
|
1968
|
+
|
|
1969
|
+
def _streamed_generate(
|
|
1970
|
+
self, messages, stop_sequences=None, response_format=None,
|
|
1971
|
+
tools_to_call_from=None, **kwargs,
|
|
1972
|
+
):
|
|
1973
|
+
from smolagents.models import ChatMessage, TokenUsage # local: version-safe
|
|
1974
|
+
completion_kwargs = self._prepare_completion_kwargs(
|
|
1975
|
+
messages=messages,
|
|
1976
|
+
stop_sequences=stop_sequences,
|
|
1977
|
+
response_format=response_format,
|
|
1978
|
+
tools_to_call_from=tools_to_call_from,
|
|
1979
|
+
model=self.model_id,
|
|
1980
|
+
custom_role_conversions=self.custom_role_conversions,
|
|
1981
|
+
convert_images_to_image_urls=True,
|
|
1982
|
+
**kwargs,
|
|
1983
|
+
)
|
|
1984
|
+
completion_kwargs["stream"] = True
|
|
1985
|
+
# Ask for a usage summary in the final chunk (OpenAI streaming convention;
|
|
1986
|
+
# Ollama honors it). Harmless if the server ignores it — usage stays 0.
|
|
1987
|
+
completion_kwargs["stream_options"] = {"include_usage": True}
|
|
1988
|
+
content, role, in_tok, out_tok, reasoning_len = self._run_stream(completion_kwargs)
|
|
1989
|
+
# Empty-turn retry: reasoning models (gemma4 on Ollama) sometimes spend the
|
|
1990
|
+
# WHOLE turn in the reasoning channel and emit no content → smolagents parses an
|
|
1991
|
+
# empty tool call and WASTES the step (seen live: 2 of 5 steps burned this way).
|
|
1992
|
+
# Retry ONCE with a hard "emit the code block now" directive appended to the
|
|
1993
|
+
# already-prepared API messages (plain dicts — no smolagents format guessing).
|
|
1994
|
+
if not content.strip():
|
|
1995
|
+
_log(f"empty-content turn (reasoning={reasoning_len} chars, no message) "
|
|
1996
|
+
"→ retrying once with a 'code now' directive")
|
|
1997
|
+
_emit({"type": "step",
|
|
1998
|
+
"text": "Model produced only analysis — nudging it to write the action…"})
|
|
1999
|
+
retry_kwargs = dict(completion_kwargs)
|
|
2000
|
+
retry_kwargs["messages"] = list(completion_kwargs.get("messages") or []) + [
|
|
2001
|
+
{"role": "user", "content": self._CODE_NOW_DIRECTIVE}
|
|
2002
|
+
]
|
|
2003
|
+
r_content, r_role, r_in, r_out, _ = self._run_stream(retry_kwargs)
|
|
2004
|
+
if r_content.strip():
|
|
2005
|
+
content, role = r_content, r_role
|
|
2006
|
+
# Count both calls' output so token accounting isn't understated.
|
|
2007
|
+
in_tok, out_tok = (r_in or in_tok), out_tok + r_out
|
|
1950
2008
|
if stop_sequences and not self.supports_stop_parameter:
|
|
1951
2009
|
from smolagents.models import remove_content_after_stop_sequences
|
|
1952
2010
|
content = remove_content_after_stop_sequences(content, stop_sequences)
|
|
1953
|
-
_log(f"streamed generate assembled {len(content)} chars "
|
|
1954
|
-
f"(in={in_tok} out={out_tok} tokens)")
|
|
1955
2011
|
return ChatMessage(
|
|
1956
2012
|
role=role,
|
|
1957
2013
|
content=content,
|
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.148",
|
|
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",
|