@tiens.nguyen/gonext-local-worker 1.0.145 → 1.0.146
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 +9 -0
- package/gonext_agent_chat.py +92 -36
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1554,6 +1554,15 @@ async function runAgentChatJob(job) {
|
|
|
1554
1554
|
await runProcessWithStreamingStdout(python, [scriptPath], input, timeoutMs, (event) => {
|
|
1555
1555
|
if (event.type === "log" && typeof event.text === "string") {
|
|
1556
1556
|
console.log(`[gonext-agent] ${event.text}`);
|
|
1557
|
+
} else if (event.type === "stream" && typeof event.text === "string") {
|
|
1558
|
+
// Live model token stream (the agent's Thought / reasoning as it generates).
|
|
1559
|
+
// Goes inside the same <think> block as the step summaries, but RAW — no
|
|
1560
|
+
// "\n\n" separator, so the tokens concatenate into flowing text.
|
|
1561
|
+
if (!inThink) {
|
|
1562
|
+
inThink = true;
|
|
1563
|
+
enqueueText("<think>");
|
|
1564
|
+
}
|
|
1565
|
+
enqueueText(event.text);
|
|
1557
1566
|
} else if (event.type === "step" && typeof event.text === "string") {
|
|
1558
1567
|
if (!inThink) {
|
|
1559
1568
|
inThink = true;
|
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.146",
|
|
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",
|