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.
Files changed (41) hide show
  1. hypernix/__init__.py +1 -1
  2. hypernix/hyped_pro.js +139 -0
  3. hypernix/interfaces/cli.py +103 -3
  4. hypernix/interfaces/hyped_pro_bridge.py +73 -2
  5. hypernix/interfaces/hyped_pro_gui.py +158 -0
  6. hypernix/interfaces/noodle/__init__.py +12 -0
  7. hypernix/interfaces/noodle/hyped.py +521 -0
  8. hypernix/interfaces/noodle/tools.py +9 -0
  9. hypernix/monitoring/cctvtop.py +145 -44
  10. hypernix/monitoring/remote_desktop.py +647 -0
  11. hypernix/monitoring/stale_log.py +596 -0
  12. hypernix/monitoring/tvtoppro.py +374 -3
  13. hypernix/monitoring/tvtoppro_modules.py +724 -0
  14. hypernix/quant/dflash1.py +400 -0
  15. hypernix/quant/dflash1_cli.py +140 -0
  16. hypernix/quant/dflash2.py +49 -39
  17. hypernix/quant/gguf.py +6 -0
  18. hypernix/quant/ggufcheck.py +12 -11
  19. hypernix/quant/hyprslug.py +490 -112
  20. hypernix/quant/hyprslug_cli.py +313 -0
  21. hypernix/quant/lowbit.py +55 -1
  22. hypernix/quant/multiquant.py +684 -0
  23. hypernix/quant/multiquant_cli.py +173 -0
  24. hypernix/quant/steamroller.py +17 -0
  25. hypernix/security/keymaster.py +89 -4
  26. hypernix/system/config.py +79 -3
  27. hypernix/t1api/accounts.py +803 -0
  28. hypernix/t1api/accounts_cli.py +313 -0
  29. hypernix/t1api/app.py +46 -1
  30. hypernix/t1api/routers/__init__.py +10 -0
  31. hypernix/t1api/routers/accounts.py +705 -0
  32. hypernix/t1api/version.py +3 -3
  33. hypernix/t1api/webauth.py +357 -0
  34. hypernix/timing/spinner.py +69 -0
  35. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/METADATA +213 -23
  36. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/RECORD +41 -29
  37. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/entry_points.txt +4 -0
  38. {hypernix-0.72.5.dev1.data → hypernix-0.72.5.dev2.data}/scripts/hypernix-t1 +0 -0
  39. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/WHEEL +0 -0
  40. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/licenses/LICENSE +0 -0
  41. {hypernix-0.72.5.dev1.dist-info → hypernix-0.72.5.dev2.dist-info}/top_level.txt +0 -0
hypernix/__init__.py CHANGED
@@ -63,7 +63,7 @@ import importlib.util
63
63
  import sys
64
64
  from typing import TYPE_CHECKING, Any
65
65
 
66
- __version__ = "0.72.5.dev1"
66
+ __version__ = "0.72.5.dev2"
67
67
  DEFAULT_REPO_ID = "ray0rf1re/hyper-Nix.2"
68
68
  DEFAULT_MODEL = "qwen3.5-4b" # New default model
69
69
 
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) {
@@ -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 -> print help instead of falling back to 'all'
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
- _print_usage()
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.8.1.0"
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
- BACKGROUND_COMMANDS = frozenset({"chat", "download", "t1api_status"})
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
  ]