@tiens.nguyen/gu-cli 1.0.723 → 1.0.724
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/client-expert-api.mjs +14 -0
- package/gonext_agent_chat.py +87 -83
- package/package.json +1 -1
package/client-expert-api.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
21
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
22
22
|
import { spawnSurviving, pidOnPort } from "./platform-tools.mjs";
|
|
23
|
+
import { DEFAULT_API_BASE as HOSTED_API_BASE } from "./device-login.mjs";
|
|
23
24
|
import { spawn } from "node:child_process";
|
|
24
25
|
import { writeFile, mkdir, readFile, access } from "node:fs/promises";
|
|
25
26
|
import { homedir } from "node:os";
|
|
@@ -64,6 +65,8 @@ export function apiEnv({
|
|
|
64
65
|
mongoUri = LOCAL_MONGO_URI,
|
|
65
66
|
dbName = LOCAL_DB_NAME,
|
|
66
67
|
base = {},
|
|
68
|
+
hostedBase = hostedApiBase(),
|
|
69
|
+
home = homedir(),
|
|
67
70
|
} = {}) {
|
|
68
71
|
return {
|
|
69
72
|
...base,
|
|
@@ -71,9 +74,20 @@ export function apiEnv({
|
|
|
71
74
|
DATA_BACKEND: "mongo",
|
|
72
75
|
MONGODB_URI: mongoUri,
|
|
73
76
|
MONGODB_DB: dbName,
|
|
77
|
+
// THE AGENT'S PROMPT SCAFFOLD IS NOT ON THIS MACHINE (task #246). The local API forwards the
|
|
78
|
+
// worker's request to the hosted API, signed with this machine's gu CLI token, and keeps the
|
|
79
|
+
// answer in memory only — so private mode needs internet for a turn, and says so when it has
|
|
80
|
+
// none. No local copy exists to fall back to, by design.
|
|
81
|
+
PROMPT_SCAFFOLD_UPSTREAM: hostedBase,
|
|
82
|
+
GU_CLI_TOKEN_FILE: join(home, ".gonext", "cli-token.json"),
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
/** The hosted gu API — where a Client Expert machine's token is renewed and its scaffold fetched. */
|
|
87
|
+
export function hostedApiBase(env = process.env) {
|
|
88
|
+
return String(env.GU_TOKEN_API_BASE ?? "").trim() || HOSTED_API_BASE;
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
/**
|
|
78
92
|
* Is this health body a usable API?
|
|
79
93
|
*
|
package/gonext_agent_chat.py
CHANGED
|
@@ -1308,7 +1308,7 @@ def _render_numbered_tool_list(tools) -> str:
|
|
|
1308
1308
|
when RAG or a workspace is enabled/disabled) and the numbering is always exactly
|
|
1309
1309
|
correct, unlike the old manually-incremented counter it replaces. Kept intentionally
|
|
1310
1310
|
terse — first docstring line only, bare arg names — since the system prompt's own
|
|
1311
|
-
`tool.to_code_prompt()` rendering (
|
|
1311
|
+
`tool.to_code_prompt()` rendering (in the API-served prompt scaffold) already shows the
|
|
1312
1312
|
full typed signature on every model call; this is just a quick-reference index."""
|
|
1313
1313
|
lines = []
|
|
1314
1314
|
for i, t in enumerate(tools, 1):
|
|
@@ -1722,81 +1722,73 @@ def _looks_like_scratchpad(stripped: str) -> bool:
|
|
|
1722
1722
|
return len(_DELIBERATION_RE.findall(stripped)) >= 3
|
|
1723
1723
|
|
|
1724
1724
|
|
|
1725
|
-
#
|
|
1726
|
-
#
|
|
1727
|
-
#
|
|
1728
|
-
#
|
|
1729
|
-
# the
|
|
1730
|
-
#
|
|
1731
|
-
#
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
"
|
|
1736
|
-
"
|
|
1737
|
-
"
|
|
1738
|
-
"{{
|
|
1739
|
-
"
|
|
1740
|
-
"- BATCH INDEPENDENT CALLS INTO ONE BLOCK. If you already know the arguments for "
|
|
1741
|
-
"several lookups, do them in ONE step and print each under a header — up to about 8 at "
|
|
1742
|
-
"a time, then continue in the next step. Split into separate steps only when an "
|
|
1743
|
-
"argument DEPENDS on a previous Observation.\n"
|
|
1744
|
-
"- Batch LOOKUPS, not changes. If a line raises, the lines after it never run — so "
|
|
1745
|
-
"keep edits, file writes and commands to one per block.\n"
|
|
1746
|
-
"- You then receive the 'Observation:'. Use it to decide the next step. When you have "
|
|
1747
|
-
"the answer, call final_answer(answer) inside a code block.\n\n"
|
|
1748
|
-
"Example — arguments already known, so ONE step does all of it:\n"
|
|
1749
|
-
"Thought: I need these three files before I can change anything.\n"
|
|
1750
|
-
"{{code_block_opening_tag}}\n"
|
|
1751
|
-
'for p in ["src/main.js", "src/api.js", "test/main.test.js"]:\n'
|
|
1752
|
-
' print("=====", p)\n'
|
|
1753
|
-
" print(read_file_lines(path=p))\n"
|
|
1754
|
-
"{{code_block_closing_tag}}\n"
|
|
1755
|
-
"Observation: \"===== src/main.js … ===== src/api.js … \"\n\n"
|
|
1756
|
-
"Example — the next call DEPENDS on this result, so it is a separate step:\n"
|
|
1757
|
-
"Thought: I'll read the page the user gave.\n"
|
|
1758
|
-
"{{code_block_opening_tag}}\n"
|
|
1759
|
-
'text = fetch_url("https://example.com")\n'
|
|
1760
|
-
"print(text)\n"
|
|
1761
|
-
"{{code_block_closing_tag}}\n"
|
|
1762
|
-
"Observation: \"Example Domain … \"\n"
|
|
1763
|
-
"Thought: I have the content, so I'll answer.\n"
|
|
1764
|
-
"{{code_block_opening_tag}}\n"
|
|
1765
|
-
'final_answer("The page is Example Domain, a placeholder site.")\n'
|
|
1766
|
-
"{{code_block_closing_tag}}\n\n"
|
|
1767
|
-
"You have access to these tools — call them as plain Python functions with the exact "
|
|
1768
|
-
"signatures shown:\n"
|
|
1769
|
-
"{{code_block_opening_tag}}\n"
|
|
1770
|
-
"{%- for tool in tools.values() %}\n"
|
|
1771
|
-
"{{ tool.to_code_prompt() }}\n"
|
|
1772
|
-
"{% endfor %}\n"
|
|
1773
|
-
"{{code_block_closing_tag}}\n"
|
|
1774
|
-
"{%- if managed_agents and managed_agents.values() | list %}\n"
|
|
1775
|
-
"You can also delegate to team members by calling them like a tool with a 'task' "
|
|
1776
|
-
"string argument:\n"
|
|
1777
|
-
"{{code_block_opening_tag}}\n"
|
|
1778
|
-
"{%- for agent in managed_agents.values() %}\n"
|
|
1779
|
-
"def {{ agent.name }}(task: str) -> str:\n"
|
|
1780
|
-
' """{{ agent.description }}"""\n'
|
|
1781
|
-
"{% endfor %}\n"
|
|
1782
|
-
"{{code_block_closing_tag}}\n"
|
|
1783
|
-
"{%- endif %}\n\n"
|
|
1784
|
-
"Rules:\n"
|
|
1785
|
-
"1. ALWAYS write a 'Thought:' line, then a code block opening with "
|
|
1786
|
-
"{{code_block_opening_tag}} and closing with {{code_block_closing_tag}} — or you fail.\n"
|
|
1787
|
-
"2. Pass tool arguments directly: calculate(expression=\"2+2\"), NOT as a dict.\n"
|
|
1788
|
-
"3. Use only variables you have defined; state persists between steps.\n"
|
|
1789
|
-
"4. Never re-run a tool call with the exact same parameters.\n"
|
|
1790
|
-
"5. Don't name a variable after a tool (e.g. 'final_answer').\n"
|
|
1791
|
-
"6. Imports are allowed ONLY from: {{authorized_imports}}\n"
|
|
1792
|
-
"7. Don't give up — you are in charge of solving the task.\n"
|
|
1793
|
-
"{%- if custom_instructions %}\n"
|
|
1794
|
-
"{{custom_instructions}}\n"
|
|
1795
|
-
"{%- endif %}\n\n"
|
|
1796
|
-
"Now Begin!"
|
|
1725
|
+
# THE CODE-AGENT SCAFFOLD IS NOT IN THIS FILE (task #246). The fixed system prompt — the
|
|
1726
|
+
# Thought → Code → Observation rules that replace smolagents' stock ~9.9k-char template — is served
|
|
1727
|
+
# by the API (GET /api/worker/prompt-scaffold) and never shipped in the gu package. The worker
|
|
1728
|
+
# fetches it once per job; if it cannot, the turn stops with the reason. There is deliberately NO
|
|
1729
|
+
# local copy to fall back to: the point is that the text is not on the user's machine.
|
|
1730
|
+
#
|
|
1731
|
+
# What a scaffold must keep, checked again here after the API checked it: without the code-block
|
|
1732
|
+
# tags the model's code is never parsed, without the tool loop no signature is shown, without
|
|
1733
|
+
# custom_instructions the per-turn hint is dropped — each fails silently, every turn.
|
|
1734
|
+
_SCAFFOLD_REQUIRED = (
|
|
1735
|
+
"{{code_block_opening_tag}}",
|
|
1736
|
+
"{{code_block_closing_tag}}",
|
|
1737
|
+
"tool.to_code_prompt()",
|
|
1738
|
+
"{{authorized_imports}}",
|
|
1739
|
+
"custom_instructions",
|
|
1797
1740
|
)
|
|
1798
1741
|
|
|
1799
1742
|
|
|
1743
|
+
# The version of the scaffold this job is running — for the log and the prompt record, never the text.
|
|
1744
|
+
_PROMPT_SCAFFOLD_VERSION = {"v": None}
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
def _fetch_prompt_scaffold(api_base: str, worker_key: str, name: str = "code-agent"):
|
|
1748
|
+
"""The agent's prompt scaffold from the API → (template, version).
|
|
1749
|
+
|
|
1750
|
+
Raises _AgentConfigError with a message the USER reads (it becomes a TURN_FAILED line) when
|
|
1751
|
+
the API cannot be asked, refuses, or sends something unusable. Never returns a default. The
|
|
1752
|
+
message never contains the template itself."""
|
|
1753
|
+
base = (api_base or "").rstrip("/")
|
|
1754
|
+
if not base or not worker_key:
|
|
1755
|
+
raise _AgentConfigError(
|
|
1756
|
+
"The agent's prompt could not be loaded: this worker has no API address or key.")
|
|
1757
|
+
from urllib.parse import urlencode
|
|
1758
|
+
req = urllib.request.Request(
|
|
1759
|
+
f"{base}/api/worker/prompt-scaffold?{urlencode({'name': name})}",
|
|
1760
|
+
headers={"X-Worker-Key": worker_key}, method="GET")
|
|
1761
|
+
try:
|
|
1762
|
+
with urllib.request.urlopen(req, timeout=15, context=_ssl_context()) as resp:
|
|
1763
|
+
data = json.loads(resp.read().decode("utf-8", "replace") or "{}")
|
|
1764
|
+
except urllib.error.HTTPError as he:
|
|
1765
|
+
detail = ""
|
|
1766
|
+
try:
|
|
1767
|
+
detail = str(json.loads(he.read().decode("utf-8", "replace") or "{}").get("error") or "")
|
|
1768
|
+
except Exception: # noqa: BLE001
|
|
1769
|
+
pass
|
|
1770
|
+
if he.code == 404 and not detail:
|
|
1771
|
+
detail = ("this API is older than the prompt service — update it "
|
|
1772
|
+
"(`gu api upgrade` on a private-mode machine)")
|
|
1773
|
+
raise _AgentConfigError(
|
|
1774
|
+
f"The agent's prompt could not be loaded (HTTP {he.code})"
|
|
1775
|
+
+ (f": {detail}" if detail else ".")) from None
|
|
1776
|
+
except Exception as e: # noqa: BLE001
|
|
1777
|
+
raise _AgentConfigError(
|
|
1778
|
+
f"The agent's prompt could not be loaded — the API at {base} did not answer "
|
|
1779
|
+
f"({type(e).__name__}). Check that it is running and reachable.") from None
|
|
1780
|
+
template = data.get("template") if isinstance(data, dict) else None
|
|
1781
|
+
version = data.get("version") if isinstance(data, dict) else None
|
|
1782
|
+
if not isinstance(template, str) or not template.strip():
|
|
1783
|
+
raise _AgentConfigError("The agent's prompt could not be loaded: the API sent an empty one.")
|
|
1784
|
+
missing = [p for p in _SCAFFOLD_REQUIRED if p not in template]
|
|
1785
|
+
if missing:
|
|
1786
|
+
raise _AgentConfigError(
|
|
1787
|
+
"The agent's prompt could not be used: it is missing " + ", ".join(missing) + ".")
|
|
1788
|
+
_PROMPT_SCAFFOLD_VERSION["v"] = version
|
|
1789
|
+
return template, version
|
|
1790
|
+
|
|
1791
|
+
|
|
1800
1792
|
def _usage_field(obj, name):
|
|
1801
1793
|
"""One field off a usage object that may be an attribute bag OR a plain dict — both
|
|
1802
1794
|
shapes come back from OpenAI-compatible clients depending on the transport."""
|
|
@@ -2300,6 +2292,14 @@ def _render_prompt(messages, max_chars: int = 400_000) -> str:
|
|
|
2300
2292
|
out = []
|
|
2301
2293
|
for m in messages or []:
|
|
2302
2294
|
role, text = _message_role_text(m)
|
|
2295
|
+
if role == "system":
|
|
2296
|
+
# THE SCAFFOLD IS NEVER RECORDED (task #246). The system message IS the API-served
|
|
2297
|
+
# prompt scaffold (rendered with the tool signatures), and this record is stored by
|
|
2298
|
+
# the API and shown on the token-usage page — so it would hand the hidden text to
|
|
2299
|
+
# anyone who opens a step. Its size stays visible, the text does not.
|
|
2300
|
+
v = _PROMPT_SCAFFOLD_VERSION["v"]
|
|
2301
|
+
text = (f"[prompt scaffold{f' v{v}' if v is not None else ''} with the tool signatures — "
|
|
2302
|
+
f"{len(text)} characters, not shown]")
|
|
2303
2303
|
out.append(f"───── {role or 'message'} ─────\n{text}")
|
|
2304
2304
|
rendered = "\n\n".join(out)
|
|
2305
2305
|
if len(rendered) > max_chars:
|
|
@@ -12111,21 +12111,25 @@ def run_agent_chat(cfg):
|
|
|
12111
12111
|
agent_kwargs["additional_authorized_imports"] = [
|
|
12112
12112
|
"json", "base64", "urllib", "urllib.request", "urllib.error"
|
|
12113
12113
|
]
|
|
12114
|
-
#
|
|
12115
|
-
#
|
|
12116
|
-
#
|
|
12117
|
-
#
|
|
12114
|
+
# THE SYSTEM PROMPT IS THE API'S SCAFFOLD (task #246): the stock templates are loaded
|
|
12115
|
+
# for everything else (planning, final-answer, managed agents) and ONLY system_prompt
|
|
12116
|
+
# is swapped. No fallback in either direction — a missing scaffold, or stock
|
|
12117
|
+
# templates that will not load, stop the turn with the reason (_AgentConfigError →
|
|
12118
|
+
# TURN_FAILED) instead of quietly running on a different prompt. The version is
|
|
12119
|
+
# logged; the text never is.
|
|
12120
|
+
_scaffold, _scaffold_version = _fetch_prompt_scaffold(worker_api_base, worker_api_key)
|
|
12118
12121
|
try:
|
|
12119
12122
|
import yaml as _yaml
|
|
12120
12123
|
import os as _os
|
|
12121
12124
|
import smolagents as _sm
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
_tpl["system_prompt"] = _COMPACT_CODE_SYSTEM_PROMPT
|
|
12125
|
-
agent_kwargs["prompt_templates"] = _tpl
|
|
12126
|
-
_log("using compact CodeAgent system prompt (trimmed few-shot examples)")
|
|
12125
|
+
with open(_os.path.join(_os.path.dirname(_sm.__file__), "prompts", "code_agent.yaml")) as _f:
|
|
12126
|
+
_tpl = _yaml.safe_load(_f)
|
|
12127
12127
|
except Exception as _e: # noqa: BLE001
|
|
12128
|
-
|
|
12128
|
+
raise _AgentConfigError(
|
|
12129
|
+
f"The agent's prompt templates could not be loaded ({type(_e).__name__}).") from None
|
|
12130
|
+
_tpl["system_prompt"] = _scaffold
|
|
12131
|
+
agent_kwargs["prompt_templates"] = _tpl
|
|
12132
|
+
_log(f"prompt scaffold code-agent v{_scaffold_version}")
|
|
12129
12133
|
agent = _ToolAgent(**agent_kwargs)
|
|
12130
12134
|
_agent_ref["a"] = agent # let step_callback trim this agent's step memory (#63)
|
|
12131
12135
|
# At the step budget the run ends WITHOUT final_answer (_maxsteps_partial hit). Offer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.724",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|