From a90223b98a7f663e824e3665029c0bbdd9cd6198 Mon Sep 17 00:00:00 2001 From: fluffy314 Date: Mon, 20 Jul 2026 17:33:38 +0800 Subject: [PATCH] fix(autoresearch): show strategy inference progress Expose Strategy Prefill and decode activity so the AutoResearch planning phase remains observable before the live GAN experiment begins. Co-authored-by: Cursor --- autoresearch/prefill/supervisor.py | 86 ++++++++++++++++++- .../bench/test_autoresearch_supervisor.py | 24 ++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/autoresearch/prefill/supervisor.py b/autoresearch/prefill/supervisor.py index d42b83c..39094dc 100644 --- a/autoresearch/prefill/supervisor.py +++ b/autoresearch/prefill/supervisor.py @@ -139,6 +139,76 @@ def parse_research_verdict(output: str, candidate_id: str) -> dict: } +class StrategyPrefillHeartbeat: + def __init__( + self, + dashboard: str = "http://127.0.0.1:8090", + interval_s: float = 10.0, + ) -> None: + self.dashboard = dashboard.rstrip("/") + self.interval_s = interval_s + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._baseline: dict = {} + self._last: tuple | None = None + + def __enter__(self): + try: + self._baseline = _json_request( + f"{self.dashboard}/v1/network/summary", + ).get("prefill", {}) + except Exception as exc: + print( + "[autoresearch] Strategy Prefill telemetry warning: " + f"{type(exc).__name__}: {exc}", + flush=True, + ) + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + return self + + def __exit__(self, *_exc) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=self.interval_s + 2) + self._emit() + + def _run(self) -> None: + while not self._stop.wait(self.interval_s): + self._emit() + + def _delta(self, current: dict, name: str) -> int: + return max( + 0, + int(current.get(name, 0)) - int(self._baseline.get(name, 0)), + ) + + def _emit(self) -> None: + try: + current = _json_request( + f"{self.dashboard}/v1/network/summary", + ).get("prefill", {}) + except Exception: + return + total = self._delta(current, "remote_job_tokens_total") + computed = self._delta(current, "remote_job_tokens_computed") + state = ( + computed, + total, + self._delta(current, "remote_hits"), + self._delta(current, "tokens_reused"), + ) + if not total or state == self._last: + return + self._last = state + percent = min(100.0, 100.0 * computed / total) + print( + f"[autoresearch] Strategy Prefill: {computed}/{total} tokens " + f"({percent:.1f}%) ยท remote_hits={state[2]} reused={state[3]}", + flush=True, + ) + + def propose_candidate( *, address: str, @@ -175,15 +245,29 @@ def propose_candidate( enable_thinking=False, ) generated: list[int] = [] + print( + f"[autoresearch] Strategy Prefill: 0/{len(ids)} tokens (0.0%)", + flush=True, + ) with Client(address) as client: with client.create_session( eos_token_ids=_resolve_eos_token_ids(tokenizer), client_label="autoresearch-strategy", ) as session: - session.append(ids) + with StrategyPrefillHeartbeat(): + session.append(ids) + print( + f"[autoresearch] Strategy Prefill complete: {len(ids)} tokens", + flush=True, + ) while len(generated) < 2048: before = len(generated) generated.extend(int(token) for token in session.generate(max_tokens=64)) + print( + f"[autoresearch] Strategy Decode: {len(generated)} tokens " + f"stop_reason={session.last_stop_reason}", + flush=True, + ) if session.last_stop_reason != 1: break if len(generated) == before: diff --git a/tests/inference_engine/bench/test_autoresearch_supervisor.py b/tests/inference_engine/bench/test_autoresearch_supervisor.py index d9e71d8..dda0468 100644 --- a/tests/inference_engine/bench/test_autoresearch_supervisor.py +++ b/tests/inference_engine/bench/test_autoresearch_supervisor.py @@ -6,6 +6,7 @@ read_results, render_candidate, should_keep, + StrategyPrefillHeartbeat, validate_candidate, ) from pathlib import Path @@ -164,6 +165,29 @@ def fake_run(command, **kwargs): assert "a[i+1]='128'" in remote +def test_strategy_prefill_heartbeat_reports_delta(monkeypatch, capsys): + heartbeat = StrategyPrefillHeartbeat(interval_s=0.01) + heartbeat._baseline = { + "remote_job_tokens_total": 100, + "remote_job_tokens_computed": 100, + "remote_hits": 2, + "tokens_reused": 20, + } + monkeypatch.setattr( + "autoresearch.prefill.supervisor._json_request", + lambda _url: {"prefill": { + "remote_job_tokens_total": 300, + "remote_job_tokens_computed": 228, + "remote_hits": 3, + "tokens_reused": 84, + }}, + ) + heartbeat._emit() + output = capsys.readouterr().out + assert "128/200 tokens (64.0%)" in output + assert "remote_hits=1 reused=64" in output + + def test_supervisor_predeploys_before_real_strategy_proposal(): source = ( Path(__file__).resolve().parents[3]