hypernix 0.72.5.dev1__py3-none-any.whl → 0.72.5.dev2__py3-none-any.whl
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.
- hypernix/__init__.py +1 -1
- hypernix/hyped_pro.js +139 -0
- hypernix/interfaces/cli.py +103 -3
- hypernix/interfaces/hyped_pro_bridge.py +73 -2
- hypernix/interfaces/hyped_pro_gui.py +158 -0
- hypernix/interfaces/noodle/__init__.py +12 -0
- hypernix/interfaces/noodle/hyped.py +521 -0
- hypernix/interfaces/noodle/tools.py +9 -0
- hypernix/monitoring/cctvtop.py +145 -44
- hypernix/monitoring/remote_desktop.py +647 -0
- hypernix/monitoring/stale_log.py +596 -0
- hypernix/monitoring/tvtoppro.py +374 -3
- hypernix/monitoring/tvtoppro_modules.py +724 -0
- hypernix/quant/dflash1.py +400 -0
- hypernix/quant/dflash1_cli.py +140 -0
- hypernix/quant/dflash2.py +49 -39
- hypernix/quant/gguf.py +6 -0
- hypernix/quant/ggufcheck.py +12 -11
- hypernix/quant/hyprslug.py +490 -112
- hypernix/quant/hyprslug_cli.py +313 -0
- hypernix/quant/lowbit.py +55 -1
- hypernix/quant/multiquant.py +684 -0
- hypernix/quant/multiquant_cli.py +173 -0
- hypernix/quant/steamroller.py +17 -0
- hypernix/security/keymaster.py +89 -4
- hypernix/system/config.py +79 -3
- hypernix/t1api/accounts.py +803 -0
- hypernix/t1api/accounts_cli.py +313 -0
- hypernix/t1api/app.py +46 -1
- hypernix/t1api/routers/__init__.py +10 -0
- hypernix/t1api/routers/accounts.py +705 -0
- hypernix/t1api/version.py +3 -3
- hypernix/t1api/webauth.py +357 -0
- hypernix/timing/spinner.py +69 -0
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/METADATA +213 -23
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/RECORD +41 -29
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/entry_points.txt +4 -0
- {hypernix-0.72.5.dev1.data → hypernix-0.72.5.dev2.data}/scripts/hypernix-t1 +0 -0
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/WHEEL +0 -0
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/licenses/LICENSE +0 -0
- {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/top_level.txt +0 -0
hypernix/__init__.py
CHANGED
hypernix/hyped_pro.js
CHANGED
|
@@ -423,6 +423,7 @@ const COMMANDS = [
|
|
|
423
423
|
{ name: "/retry", desc: "Regenerate the last agent reply" },
|
|
424
424
|
{ name: "/key", desc: "Set/view a cloud API key: /key <vendor> [api-key]" },
|
|
425
425
|
{ name: "/t1api", desc: "HyperNix T1 API server: /t1api [status | url <url>]" },
|
|
426
|
+
{ name: "/noodle", desc: "Run an autonomous agent swarm: /noodle <task>, or /noodle providers" },
|
|
426
427
|
{ name: "/version", desc: "Show the installed version and the latest public release" },
|
|
427
428
|
{ name: "/settings", desc: "View/change max input, output, and thinking tokens" },
|
|
428
429
|
{ name: "/tools", desc: "Toggle file create/edit/read/search tools on or off" },
|
|
@@ -473,6 +474,11 @@ let thinkingDisplay = THINKING_DISPLAY_MODES.includes(cfg.thinkingDisplay || "")
|
|
|
473
474
|
// supports tool calling; hypernix.old_oven (the plain safetensors path)
|
|
474
475
|
// has no tool-calling support to enable regardless of this setting.
|
|
475
476
|
let toolsEnabled = cfg.toolsEnabled !== undefined ? cfg.toolsEnabled : true;
|
|
477
|
+
// The Noodle swarm currently being polled, so `/noodle stop` with no
|
|
478
|
+
// argument has something to aim at. Not persisted: a session id refers
|
|
479
|
+
// to a thread inside the bridge process, and a stale one from last time
|
|
480
|
+
// would address nothing.
|
|
481
|
+
let noodleSession = null;
|
|
476
482
|
const startTime = Date.now();
|
|
477
483
|
// Models already confirmed present on disk this session, so we don't
|
|
478
484
|
// re-check/re-download on every turn.
|
|
@@ -1147,6 +1153,139 @@ async function handleSlashCommand(cmdStr) {
|
|
|
1147
1153
|
log(out.join(NL));
|
|
1148
1154
|
break;
|
|
1149
1155
|
}
|
|
1156
|
+
// 0.72.6: Noodle. It has called itself "the autonomous executor
|
|
1157
|
+
// inside Hyped Pro" since it shipped and was not reachable from here
|
|
1158
|
+
// at all — running a swarm meant leaving this TUI and starting a
|
|
1159
|
+
// different program in another terminal.
|
|
1160
|
+
//
|
|
1161
|
+
// A run is a session, not a call. `noodle_start` returns at once and
|
|
1162
|
+
// this polls for events, so the swarm's progress lands in the
|
|
1163
|
+
// scrollback live instead of the screen freezing for five minutes
|
|
1164
|
+
// and then printing everything.
|
|
1165
|
+
case "/noodle": {
|
|
1166
|
+
const [sub, ...rest] = args;
|
|
1167
|
+
if (sub === "providers" || sub === "models") {
|
|
1168
|
+
const r = await bridge.call('noodle_providers', {});
|
|
1169
|
+
if (!r.ok) {
|
|
1170
|
+
elog(r.code || 'HPT-NOODLE-001', r.error || 'could not list providers');
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
const out = [c256(96, "\n Noodle providers:")];
|
|
1174
|
+
(r.data.providers || []).forEach((p) => {
|
|
1175
|
+
const mark = p.ready ? c256(82, "✓") : dim("✗");
|
|
1176
|
+
const model = p.default_model ? dim(` ${p.default_model}`) : "";
|
|
1177
|
+
const why = p.reason ? dim(` ${p.reason}`) : "";
|
|
1178
|
+
out.push(` ${mark} ${c256(33, String(p.provider).padEnd(10))}${model}${why}`);
|
|
1179
|
+
});
|
|
1180
|
+
if ((r.data.adopted_keys || []).length) {
|
|
1181
|
+
out.push(dim(`\n Using saved keys for: ${r.data.adopted_keys.join(", ")}`));
|
|
1182
|
+
}
|
|
1183
|
+
if (!r.data.any_ready) {
|
|
1184
|
+
out.push(c256(220, "\n Nothing is usable yet. Set a key with /key <vendor> <key>,"));
|
|
1185
|
+
out.push(c256(220, " or start Ollama for a local model."));
|
|
1186
|
+
}
|
|
1187
|
+
log(out.join(NL));
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
if (sub === "sessions" || sub === "ls") {
|
|
1191
|
+
const r = await bridge.call('noodle_sessions', {});
|
|
1192
|
+
if (!r.ok) {
|
|
1193
|
+
elog(r.code || 'HPT-NOODLE-001', r.error || 'could not list sessions');
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
const list = r.data.sessions || [];
|
|
1197
|
+
if (!list.length) {
|
|
1198
|
+
log(dim(" No Noodle sessions in this hyped-pro session yet."));
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const out = [c256(96, "\n Noodle sessions:")];
|
|
1202
|
+
list.forEach((s) => {
|
|
1203
|
+
const state = s.running ? c256(220, "running") : (s.error ? c256(196, "failed") : c256(82, "done"));
|
|
1204
|
+
out.push(` ${c256(33, s.session_id)} ${state} ${dim(`${s.elapsed}s`)} ${String(s.prompt).slice(0, 48)}`);
|
|
1205
|
+
});
|
|
1206
|
+
log(out.join(NL));
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
if (sub === "stop") {
|
|
1210
|
+
const target = rest[0] || noodleSession;
|
|
1211
|
+
if (!target) {
|
|
1212
|
+
log(dim(" Nothing to stop. /noodle sessions lists them."));
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
const r = await bridge.call('noodle_stop', { session: target });
|
|
1216
|
+
if (!r.ok) {
|
|
1217
|
+
elog(r.code || 'HPT-NOODLE-001', r.error || 'could not stop it');
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
log(c256(220, ` Asked ${target} to stop.`));
|
|
1221
|
+
// Said plainly because a "stop" that does not stop things is
|
|
1222
|
+
// worth knowing about: every provider client is blocking HTTP in
|
|
1223
|
+
// a thread pool, and a request already on the wire is paid for.
|
|
1224
|
+
if (!r.data.swarm_notified) {
|
|
1225
|
+
log(dim(" Turns already in flight will finish — they are paid for either way."));
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
const task = (sub ? [sub, ...rest] : []).join(" ").trim();
|
|
1230
|
+
if (!task) {
|
|
1231
|
+
log(c256(96, "\n Usage:"));
|
|
1232
|
+
log(" /noodle <task> run an agent swarm on it");
|
|
1233
|
+
log(" /noodle providers which models it could use");
|
|
1234
|
+
log(" /noodle sessions past and running runs");
|
|
1235
|
+
log(" /noodle stop [id] ask a run to stop");
|
|
1236
|
+
log(dim("\n Agents get file tools rooted at ./.noodle — nothing outside it,"));
|
|
1237
|
+
log(dim(" and no shell execution unless HYPED_NOODLE_EXEC=1 is set."));
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
// Off by default and deliberately not a flag on the command. A
|
|
1241
|
+
// chat TUI is a place people paste prompts without reading them
|
|
1242
|
+
// closely, and "run whatever shell commands you like" should take
|
|
1243
|
+
// more than a typo to switch on.
|
|
1244
|
+
const allowExec = process.env.HYPED_NOODLE_EXEC === "1";
|
|
1245
|
+
log(c256(96, `\n Noodle: ${task}`));
|
|
1246
|
+
if (allowExec)
|
|
1247
|
+
log(c256(220, " Shell execution is ON (HYPED_NOODLE_EXEC=1)."));
|
|
1248
|
+
const started = await bridge.call('noodle_start', {
|
|
1249
|
+
prompt: task,
|
|
1250
|
+
allow_execute: allowExec,
|
|
1251
|
+
});
|
|
1252
|
+
if (!started.ok) {
|
|
1253
|
+
elog(started.code || 'HPT-NOODLE-001', started.error || 'could not start the swarm');
|
|
1254
|
+
log(dim(" /noodle providers shows what is usable here."));
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
noodleSession = started.data.session_id;
|
|
1258
|
+
log(dim(` session ${noodleSession} · roster ${(started.data.roster || []).join(", ")} · root ${started.data.root}`));
|
|
1259
|
+
// Long-poll until it finishes. The bridge answers a poll as soon
|
|
1260
|
+
// as there is anything to say, so this is a live feed rather than
|
|
1261
|
+
// a spin.
|
|
1262
|
+
let running = true;
|
|
1263
|
+
while (running) {
|
|
1264
|
+
const tick = await bridge.call('noodle_poll', { session: noodleSession, timeout: 2.0 });
|
|
1265
|
+
if (!tick.ok) {
|
|
1266
|
+
elog(tick.code || 'HPT-NOODLE-001', tick.error || 'poll failed');
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
(tick.data.events || []).forEach((ev) => {
|
|
1270
|
+
const kind = String(ev.kind || ev.type || "event");
|
|
1271
|
+
const detail = String(ev.message || ev.text || ev.tool || ev.task_id || "");
|
|
1272
|
+
log(` ${dim(kind.padEnd(16))} ${detail.slice(0, 96)}`);
|
|
1273
|
+
});
|
|
1274
|
+
running = Boolean(tick.data.running);
|
|
1275
|
+
if (!running) {
|
|
1276
|
+
if (tick.data.error) {
|
|
1277
|
+
log(c256(196, ` Swarm failed: ${tick.data.error}`));
|
|
1278
|
+
}
|
|
1279
|
+
else if (tick.data.report) {
|
|
1280
|
+
const rep = tick.data.report;
|
|
1281
|
+
const ok = rep.ok ? c256(82, "all tasks succeeded") : c256(220, `${(rep.failed || []).length} task(s) failed`);
|
|
1282
|
+
log(` ${ok} ${dim(`in ${tick.data.elapsed}s`)}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
noodleSession = null;
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1150
1289
|
case "/settings": {
|
|
1151
1290
|
const SETTINGS_KEYS = ["max-input-tokens", "max-output-tokens", "max-thinking-tokens", "thinking-display"];
|
|
1152
1291
|
if (!argText) {
|
hypernix/interfaces/cli.py
CHANGED
|
@@ -113,6 +113,87 @@ _SUBCOMMANDS = {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
#: Spellings that are not subcommands but are obviously meant as one.
|
|
117
|
+
#:
|
|
118
|
+
#: Separate from difflib's fuzzy matching because these are not typos --
|
|
119
|
+
#: they are the other correct spelling, or the name of the tool rather
|
|
120
|
+
#: than the name of the subcommand. "quantise" is British English and
|
|
121
|
+
#: this package's own docstrings use it throughout; "tvtoppro" is a
|
|
122
|
+
#: console script people reasonably expect to work as a subcommand too.
|
|
123
|
+
_COMMAND_ALIASES: dict[str, str] = {
|
|
124
|
+
"quantise": "quantize",
|
|
125
|
+
"quant": "quantize",
|
|
126
|
+
"q": "quantize",
|
|
127
|
+
"dl": "download",
|
|
128
|
+
"get": "download",
|
|
129
|
+
"pull": "download",
|
|
130
|
+
"conv": "convert",
|
|
131
|
+
"gguf": "convert",
|
|
132
|
+
"ls": "info",
|
|
133
|
+
"list": "info",
|
|
134
|
+
"version": "--version",
|
|
135
|
+
"help": "--help",
|
|
136
|
+
"tvtoppro": "tvtop",
|
|
137
|
+
"tvtop-pro": "tvtop",
|
|
138
|
+
"hyprslug": "quantize",
|
|
139
|
+
"doomslug": "quantize",
|
|
140
|
+
"keys": "gkey",
|
|
141
|
+
"key": "gkey",
|
|
142
|
+
"docs": "wiki",
|
|
143
|
+
"diagnose": "doctor",
|
|
144
|
+
"check": "doctor",
|
|
145
|
+
"dashboard": "cctvtop",
|
|
146
|
+
"monitor": "cctvtop",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _suggest(typed: str, limit: int = 3) -> list[str]:
|
|
151
|
+
"""Subcommands *typed* was probably meant to be.
|
|
152
|
+
|
|
153
|
+
Prefix matches first and exactly: somebody who typed ``down`` meant
|
|
154
|
+
``download`` and difflib's ratio does not reliably rank a prefix
|
|
155
|
+
above a similarly-sized edit. Fuzzy matches fill the rest.
|
|
156
|
+
"""
|
|
157
|
+
import difflib
|
|
158
|
+
|
|
159
|
+
lowered = typed.lower()
|
|
160
|
+
known = sorted(_SUBCOMMANDS)
|
|
161
|
+
prefix = [name for name in known if name.startswith(lowered)]
|
|
162
|
+
contains = [
|
|
163
|
+
name for name in known
|
|
164
|
+
if lowered in name and name not in prefix and len(lowered) >= 3
|
|
165
|
+
]
|
|
166
|
+
fuzzy = [
|
|
167
|
+
name for name in difflib.get_close_matches(lowered, known, n=limit, cutoff=0.6)
|
|
168
|
+
if name not in prefix and name not in contains
|
|
169
|
+
]
|
|
170
|
+
ordered: list[str] = []
|
|
171
|
+
for name in (*prefix, *contains, *fuzzy):
|
|
172
|
+
if name not in ordered:
|
|
173
|
+
ordered.append(name)
|
|
174
|
+
return ordered[:limit]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _unknown_command(typed: str) -> int:
|
|
178
|
+
"""Say what was wrong with it, and what to type instead."""
|
|
179
|
+
print(f"hypernix: unknown command {typed!r}", file=sys.stderr)
|
|
180
|
+
|
|
181
|
+
alias = _COMMAND_ALIASES.get(typed.lower())
|
|
182
|
+
if alias:
|
|
183
|
+
print(f"\n You want: hypernix {alias}", file=sys.stderr)
|
|
184
|
+
else:
|
|
185
|
+
suggestions = _suggest(typed)
|
|
186
|
+
if len(suggestions) == 1:
|
|
187
|
+
print(f"\n Did you mean: hypernix {suggestions[0]}", file=sys.stderr)
|
|
188
|
+
elif suggestions:
|
|
189
|
+
print("\n Did you mean one of these?", file=sys.stderr)
|
|
190
|
+
for name in suggestions:
|
|
191
|
+
print(f" hypernix {name}", file=sys.stderr)
|
|
192
|
+
|
|
193
|
+
print("\n hypernix --help every subcommand", file=sys.stderr)
|
|
194
|
+
return 2
|
|
195
|
+
|
|
196
|
+
|
|
116
197
|
def _print_usage() -> None:
|
|
117
198
|
try:
|
|
118
199
|
from rich.console import Console
|
|
@@ -162,10 +243,23 @@ def _print_usage() -> None:
|
|
|
162
243
|
table.add_row("[green]config[/]", "Configuration management")
|
|
163
244
|
table.add_row("[green]gkey[/]", "API key & access management (Gatekeeper + Keymaster)")
|
|
164
245
|
table.add_row("[green]map[/]", "Steampunk schematic TUI: dials/pipes/steam for model + training state")
|
|
246
|
+
# These four dispatch and always have; they were simply never
|
|
247
|
+
# added to the menu, so `hypernix --help` was not a list of what
|
|
248
|
+
# hypernix can do and there was no way to find them at all.
|
|
249
|
+
table.add_row("[green]devices[/]", "list the GPUs/accelerators this machine has, and what they support")
|
|
250
|
+
table.add_row("[green]wakeup[/]", "wake-word detection: train a trigger phrase, then listen for it")
|
|
251
|
+
table.add_row("[green]websearch[/]", "web search backend used by the chat and assistant surfaces")
|
|
252
|
+
table.add_row("[green]hyprslug-headers[/]", "self-describing GGUF headers + the OpenAI-compatible sub-bit server")
|
|
165
253
|
|
|
166
254
|
shortcuts = Text("Shortcuts:\n", style="bold yellow")
|
|
167
255
|
shortcuts.append(" --auto-oven download the default snapshot and run code completion\n", style="white")
|
|
168
256
|
shortcuts.append(" (equivalent to `hypernix oven --auto ...`).\n", style="dim")
|
|
257
|
+
shortcuts.append("\nAlso accepted: ", style="bold yellow")
|
|
258
|
+
shortcuts.append(
|
|
259
|
+
"tvtop (cctvtop) · fiz (fizzle) · camouflage (camo) · protect (prot) ·\n"
|
|
260
|
+
" fuse-box (fusebox)\n",
|
|
261
|
+
style="dim",
|
|
262
|
+
)
|
|
169
263
|
|
|
170
264
|
help_text = Text("\nRun `hypernix <subcommand> --help` for per-command flags.\nRun `hypernix all --help` for the classic pipeline flags.", style="italic dim")
|
|
171
265
|
|
|
@@ -1070,10 +1164,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1070
1164
|
if raw[0] == "--auto-oven":
|
|
1071
1165
|
return _run_oven(["--auto", *raw[1:]])
|
|
1072
1166
|
|
|
1073
|
-
# First arg isn't a subcommand
|
|
1167
|
+
# First arg isn't a subcommand.
|
|
1168
|
+
#
|
|
1169
|
+
# This used to print the whole forty-row menu and exit 1, without ever
|
|
1170
|
+
# mentioning what was typed. `hypernix downlod` produced fifty lines
|
|
1171
|
+
# of table, none of which said "downlod", so the failure looked
|
|
1172
|
+
# exactly like asking for help on purpose. With this many subcommands
|
|
1173
|
+
# -- and pairs like quantize/quantise and fizzle/fiz -- a suggestion
|
|
1174
|
+
# is worth more than the whole menu.
|
|
1074
1175
|
if raw[0] not in _SUBCOMMANDS:
|
|
1075
|
-
|
|
1076
|
-
return 1
|
|
1176
|
+
return _unknown_command(raw[0])
|
|
1077
1177
|
|
|
1078
1178
|
cmd, rest = raw[0], raw[1:]
|
|
1079
1179
|
if cmd == "all":
|
|
@@ -43,7 +43,7 @@ from typing import Any
|
|
|
43
43
|
|
|
44
44
|
from hypernix.interfaces import hyped_pro_core as core
|
|
45
45
|
|
|
46
|
-
BRIDGE_VERSION = "1.0.26.
|
|
46
|
+
BRIDGE_VERSION = "1.0.26.9.2.3"
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
# ---------------------------------------------------------------------------
|
|
@@ -77,7 +77,15 @@ BRIDGE_VERSION = "1.0.26.8.1.0"
|
|
|
77
77
|
# Commands that can take long enough to be worth cancelling, and are safe to
|
|
78
78
|
# run off the main loop. Everything else is a fast config read/write and
|
|
79
79
|
# stays inline, where it can't interleave with itself.
|
|
80
|
-
|
|
80
|
+
# noodle_poll is here because it long-polls: it waits up to `timeout`
|
|
81
|
+
# seconds for an event rather than returning empty and being asked again
|
|
82
|
+
# immediately. Inline, that wait would block the stdin loop and the TUI
|
|
83
|
+
# could not deliver a cancel while a swarm was running -- the same bug
|
|
84
|
+
# that made `chat` uncancellable before it moved off the loop.
|
|
85
|
+
#
|
|
86
|
+
# noodle_start is not here. It spawns a thread and returns at once, so
|
|
87
|
+
# backgrounding it would buy a thread to start a thread.
|
|
88
|
+
BACKGROUND_COMMANDS = frozenset({"chat", "download", "t1api_status", "noodle_poll"})
|
|
81
89
|
|
|
82
90
|
# Backends whose generation loop polls should_stop. Anything else can be
|
|
83
91
|
# asked to cancel, but the request will finish first.
|
|
@@ -228,6 +236,69 @@ def dispatch(req: dict[str, Any], cancel: threading.Event | None = None) -> dict
|
|
|
228
236
|
core.set_t1_api_url(req["url"])
|
|
229
237
|
return _ok(id_, {"url": core.t1_api_url()})
|
|
230
238
|
|
|
239
|
+
# -- 0.72.5: Noodle, the autonomous executor ------------------
|
|
240
|
+
#
|
|
241
|
+
# Noodle has described itself as "the autonomous executor inside
|
|
242
|
+
# Hyped Pro" since it shipped and was not reachable from
|
|
243
|
+
# hyped-pro at all. These five verbs are the connection.
|
|
244
|
+
#
|
|
245
|
+
# A run is a session rather than a call: it takes minutes and
|
|
246
|
+
# produces nothing until it ends, so doing it inside one request
|
|
247
|
+
# would hold a bridge thread and leave the TUI with nothing to
|
|
248
|
+
# draw. `noodle_start` returns a session id at once and
|
|
249
|
+
# `noodle_poll` drains events as they happen.
|
|
250
|
+
if cmd == "noodle_providers":
|
|
251
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
252
|
+
|
|
253
|
+
return _ok(id_, noodle.providers())
|
|
254
|
+
|
|
255
|
+
if cmd == "noodle_start":
|
|
256
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
return _ok(id_, noodle.start(
|
|
260
|
+
req.get("prompt", ""),
|
|
261
|
+
roster=req.get("roster"),
|
|
262
|
+
root=req.get("root"),
|
|
263
|
+
tasks=req.get("tasks"),
|
|
264
|
+
max_parallel=int(req.get("max_parallel", 4)),
|
|
265
|
+
allow_execute=bool(req.get("allow_execute", False)),
|
|
266
|
+
allow_outside=bool(req.get("allow_outside", False)),
|
|
267
|
+
memory_enabled=bool(req.get("memory_enabled", False)),
|
|
268
|
+
max_turns=int(req.get("max_turns", 12)),
|
|
269
|
+
verify=req.get("verify", ""),
|
|
270
|
+
))
|
|
271
|
+
except noodle.NoodleSessionError as exc:
|
|
272
|
+
return _err(id_, "HPB-NOODLE-001", str(exc))
|
|
273
|
+
|
|
274
|
+
if cmd == "noodle_poll":
|
|
275
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
return _ok(id_, noodle.poll(
|
|
279
|
+
req["session"], timeout=float(req.get("timeout", 0.0))
|
|
280
|
+
))
|
|
281
|
+
except noodle.NoodleSessionError as exc:
|
|
282
|
+
return _err(id_, "HPB-NOODLE-002", str(exc))
|
|
283
|
+
|
|
284
|
+
if cmd == "noodle_stop":
|
|
285
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
return _ok(id_, noodle.stop(req["session"]))
|
|
289
|
+
except noodle.NoodleSessionError as exc:
|
|
290
|
+
return _err(id_, "HPB-NOODLE-002", str(exc))
|
|
291
|
+
|
|
292
|
+
if cmd == "noodle_sessions":
|
|
293
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
294
|
+
|
|
295
|
+
if req.get("session"):
|
|
296
|
+
try:
|
|
297
|
+
return _ok(id_, noodle.summary(req["session"]))
|
|
298
|
+
except noodle.NoodleSessionError as exc:
|
|
299
|
+
return _err(id_, "HPB-NOODLE-002", str(exc))
|
|
300
|
+
return _ok(id_, {"sessions": noodle.sessions()})
|
|
301
|
+
|
|
231
302
|
if cmd == "cancel":
|
|
232
303
|
return _ok(id_, {"target": req.get("target"),
|
|
233
304
|
"result": _cancel(req.get("target"))})
|
|
@@ -166,6 +166,7 @@ def try_launch_qt() -> bool:
|
|
|
166
166
|
def _make_main_window_class():
|
|
167
167
|
from PySide6.QtCore import QObject, QThread, Signal
|
|
168
168
|
from PySide6.QtWidgets import (
|
|
169
|
+
QCheckBox,
|
|
169
170
|
QComboBox,
|
|
170
171
|
QFormLayout,
|
|
171
172
|
QHBoxLayout,
|
|
@@ -232,9 +233,13 @@ def _make_main_window_class():
|
|
|
232
233
|
self._chat_thread: QThread | None = None
|
|
233
234
|
self._dl_thread: QThread | None = None
|
|
234
235
|
|
|
236
|
+
self._noodle_session: str | None = None
|
|
237
|
+
self._noodle_timer = None
|
|
238
|
+
|
|
235
239
|
tabs = QTabWidget()
|
|
236
240
|
self.setCentralWidget(tabs)
|
|
237
241
|
tabs.addTab(self._build_chat_tab(), "Chat")
|
|
242
|
+
tabs.addTab(self._build_noodle_tab(), "Noodle")
|
|
238
243
|
tabs.addTab(self._build_keys_tab(), "API Keys")
|
|
239
244
|
|
|
240
245
|
# -- Chat tab ---------------------------------------------------
|
|
@@ -268,6 +273,159 @@ def _make_main_window_class():
|
|
|
268
273
|
|
|
269
274
|
return w
|
|
270
275
|
|
|
276
|
+
# -- Noodle tab -------------------------------------------------
|
|
277
|
+
#
|
|
278
|
+
# The same session API the TUI drives over the bridge, called
|
|
279
|
+
# directly because the GUI is already a Python process. A QTimer
|
|
280
|
+
# polls for events rather than a thread: poll() is a lock and a
|
|
281
|
+
# deque drain, far too cheap to be worth a thread, and doing it
|
|
282
|
+
# on the UI thread keeps every widget update where Qt wants it.
|
|
283
|
+
def _build_noodle_tab(self) -> QWidget:
|
|
284
|
+
w = QWidget()
|
|
285
|
+
layout = QVBoxLayout(w)
|
|
286
|
+
|
|
287
|
+
layout.addWidget(QLabel(
|
|
288
|
+
"Noodle runs agents against a task. They get file tools rooted "
|
|
289
|
+
"at ./.noodle and nothing outside it."
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
top = QHBoxLayout()
|
|
293
|
+
self.noodle_task = QLineEdit()
|
|
294
|
+
self.noodle_task.setPlaceholderText("What should the swarm do?")
|
|
295
|
+
self.noodle_task.returnPressed.connect(self._on_noodle_start)
|
|
296
|
+
self.noodle_start_btn = QPushButton("Run")
|
|
297
|
+
self.noodle_start_btn.clicked.connect(self._on_noodle_start)
|
|
298
|
+
self.noodle_stop_btn = QPushButton("Stop")
|
|
299
|
+
self.noodle_stop_btn.setEnabled(False)
|
|
300
|
+
self.noodle_stop_btn.clicked.connect(self._on_noodle_stop)
|
|
301
|
+
top.addWidget(self.noodle_task, stretch=1)
|
|
302
|
+
top.addWidget(self.noodle_start_btn)
|
|
303
|
+
top.addWidget(self.noodle_stop_btn)
|
|
304
|
+
layout.addLayout(top)
|
|
305
|
+
|
|
306
|
+
self.noodle_exec = QCheckBox(
|
|
307
|
+
"Allow shell execution (agents may run commands)"
|
|
308
|
+
)
|
|
309
|
+
layout.addWidget(self.noodle_exec)
|
|
310
|
+
|
|
311
|
+
self.noodle_log = QTextEdit()
|
|
312
|
+
self.noodle_log.setReadOnly(True)
|
|
313
|
+
layout.addWidget(self.noodle_log, stretch=1)
|
|
314
|
+
|
|
315
|
+
self.noodle_status = QLabel("")
|
|
316
|
+
layout.addWidget(self.noodle_status)
|
|
317
|
+
|
|
318
|
+
self._refresh_noodle_providers()
|
|
319
|
+
return w
|
|
320
|
+
|
|
321
|
+
def _refresh_noodle_providers(self) -> None:
|
|
322
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
info = noodle.providers()
|
|
326
|
+
except Exception as exc: # noqa: BLE001 - a tab must still build
|
|
327
|
+
self.noodle_status.setText(f"Could not list providers: {exc}")
|
|
328
|
+
return
|
|
329
|
+
if info["any_ready"]:
|
|
330
|
+
self.noodle_status.setText(
|
|
331
|
+
"Ready: " + ", ".join(info["ready"])
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
self.noodle_status.setText(
|
|
335
|
+
"No provider is usable yet — set a key in the API Keys tab, "
|
|
336
|
+
"or start Ollama."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _noodle_append(self, text: str) -> None:
|
|
340
|
+
self.noodle_log.append(text)
|
|
341
|
+
|
|
342
|
+
def _on_noodle_start(self) -> None:
|
|
343
|
+
from PySide6.QtCore import QTimer
|
|
344
|
+
|
|
345
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
346
|
+
|
|
347
|
+
task = self.noodle_task.text().strip()
|
|
348
|
+
if not task:
|
|
349
|
+
return
|
|
350
|
+
self.noodle_log.clear()
|
|
351
|
+
try:
|
|
352
|
+
session = noodle.start(
|
|
353
|
+
task, allow_execute=self.noodle_exec.isChecked()
|
|
354
|
+
)
|
|
355
|
+
except noodle.NoodleSessionError as exc:
|
|
356
|
+
self._noodle_append(
|
|
357
|
+
f'<span style="color:#e05555">{exc}</span>'
|
|
358
|
+
)
|
|
359
|
+
return
|
|
360
|
+
self._noodle_session = session["session_id"]
|
|
361
|
+
self.noodle_start_btn.setEnabled(False)
|
|
362
|
+
self.noodle_stop_btn.setEnabled(True)
|
|
363
|
+
self.noodle_task.setEnabled(False)
|
|
364
|
+
self._noodle_append(
|
|
365
|
+
f'<i>session {session["session_id"]} · '
|
|
366
|
+
f'{", ".join(session["roster"])} · {session["root"]}</i>'
|
|
367
|
+
)
|
|
368
|
+
self._noodle_timer = QTimer(self)
|
|
369
|
+
self._noodle_timer.timeout.connect(self._on_noodle_tick)
|
|
370
|
+
self._noodle_timer.start(400)
|
|
371
|
+
|
|
372
|
+
def _on_noodle_tick(self) -> None:
|
|
373
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
374
|
+
|
|
375
|
+
if not self._noodle_session:
|
|
376
|
+
return
|
|
377
|
+
try:
|
|
378
|
+
tick = noodle.poll(self._noodle_session)
|
|
379
|
+
except noodle.NoodleSessionError as exc:
|
|
380
|
+
self._noodle_append(f'<span style="color:#e05555">{exc}</span>')
|
|
381
|
+
self._noodle_finish()
|
|
382
|
+
return
|
|
383
|
+
for event in tick["events"]:
|
|
384
|
+
kind = str(event.get("kind") or event.get("type") or "event")
|
|
385
|
+
detail = str(
|
|
386
|
+
event.get("message") or event.get("text")
|
|
387
|
+
or event.get("tool") or event.get("task_id") or ""
|
|
388
|
+
)
|
|
389
|
+
self._noodle_append(f"<b>{kind}</b> {detail}")
|
|
390
|
+
if not tick["running"]:
|
|
391
|
+
if tick["error"]:
|
|
392
|
+
self._noodle_append(
|
|
393
|
+
f'<span style="color:#e05555">{tick["error"]}</span>'
|
|
394
|
+
)
|
|
395
|
+
elif tick["report"]:
|
|
396
|
+
ok = tick["report"].get("ok")
|
|
397
|
+
self._noodle_append(
|
|
398
|
+
f'<b>{"Done" if ok else "Finished with failures"}</b> '
|
|
399
|
+
f'in {tick["elapsed"]}s'
|
|
400
|
+
)
|
|
401
|
+
self._noodle_finish()
|
|
402
|
+
|
|
403
|
+
def _on_noodle_stop(self) -> None:
|
|
404
|
+
from hypernix.interfaces.noodle import hyped as noodle
|
|
405
|
+
|
|
406
|
+
if not self._noodle_session:
|
|
407
|
+
return
|
|
408
|
+
try:
|
|
409
|
+
result = noodle.stop(self._noodle_session)
|
|
410
|
+
except noodle.NoodleSessionError as exc:
|
|
411
|
+
self._noodle_append(f'<span style="color:#e05555">{exc}</span>')
|
|
412
|
+
return
|
|
413
|
+
self._noodle_append("<i>Stopping…</i>")
|
|
414
|
+
if not result["swarm_notified"]:
|
|
415
|
+
self._noodle_append(
|
|
416
|
+
"<i>Turns already in flight will finish — they are paid "
|
|
417
|
+
"for either way.</i>"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _noodle_finish(self) -> None:
|
|
421
|
+
if self._noodle_timer is not None:
|
|
422
|
+
self._noodle_timer.stop()
|
|
423
|
+
self._noodle_timer = None
|
|
424
|
+
self._noodle_session = None
|
|
425
|
+
self.noodle_start_btn.setEnabled(True)
|
|
426
|
+
self.noodle_stop_btn.setEnabled(False)
|
|
427
|
+
self.noodle_task.setEnabled(True)
|
|
428
|
+
|
|
271
429
|
def _current_model_short(self) -> str:
|
|
272
430
|
return self.model_box.currentData()
|
|
273
431
|
|
|
@@ -29,9 +29,20 @@ to a different provider on its own (surprising invoices in one direction,
|
|
|
29
29
|
surprising output in the other), and it does not execute a client
|
|
30
30
|
application a remote server asked it to run. Both are opt-in and both
|
|
31
31
|
say what they are doing.
|
|
32
|
+
|
|
33
|
+
Inside hyped-pro
|
|
34
|
+
----------------
|
|
35
|
+
The line above about being "the autonomous executor inside Hyped Pro" was
|
|
36
|
+
aspirational until 0.72.5: Noodle was a library and a console script, and
|
|
37
|
+
hyped-pro had no command, no bridge verb and no idea it existed.
|
|
38
|
+
:mod:`hypernix.interfaces.noodle.hyped` is the connection — sessions the
|
|
39
|
+
TUI and the GUI drive with ``/noodle`` and a Noodle tab respectively,
|
|
40
|
+
with a live event feed rather than a frozen screen for the length of a
|
|
41
|
+
run.
|
|
32
42
|
"""
|
|
33
43
|
from __future__ import annotations
|
|
34
44
|
|
|
45
|
+
from . import hyped
|
|
35
46
|
from .agent import Agent, AgentEvent, AgentResult
|
|
36
47
|
from .hpo import HPOResult, SearchSpace, Trial, random_search, successive_halving
|
|
37
48
|
from .providers import (
|
|
@@ -60,4 +71,5 @@ __all__ = [
|
|
|
60
71
|
"Tool", "ToolContext", "ToolError", "ToolResult", "TOOLS", "run_tool", "tool_schemas",
|
|
61
72
|
"syntax_verifier", "command_verifier", "combine",
|
|
62
73
|
"SearchSpace", "Trial", "HPOResult", "random_search", "successive_halving",
|
|
74
|
+
"hyped",
|
|
63
75
|
]
|