Download voyager_runner.py from Erinaldordso/Minecraft-Friends-AI-ZeroGPU: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/voyager_runner.py
- Command line
-
hf download hf://spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/voyager_runner.py
-
curl -L -o voyager_runner.py https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/voyager_runner.py
16 kB
| from __future__ import annotations | |
| import os | |
| import json | |
| import logging | |
| import shutil | |
| import sys | |
| import threading | |
| import time | |
| from pathlib import Path | |
| VOYAGER_HOME = Path(os.environ.get("VOYAGER_HOME", "/tmp/voyager-runtime/Voyager")) | |
| if str(VOYAGER_HOME) not in sys.path: | |
| sys.path.insert(0, str(VOYAGER_HOME)) | |
| from voyager import Voyager # noqa: E402 | |
| from voyager_verification import ( | |
| quarantine_invalid_skills, quarantine_template_skills, task_spec, | |
| ) | |
| from voyager_creative_critic import VerifiedCreativeCritic | |
| from voyager_runtime_contract import attest_original_runtime, write_attestation | |
| from voyager_metrics import attach_role_metrics | |
| def env(name: str, default: str) -> str: | |
| return os.environ.get(name, default) | |
| def main() -> None: | |
| from voyager_learning import refresh_learning_state, start_learning_watcher | |
| # Surface Mineflayer's subprocess logs as well as Python tracebacks. | |
| logging.basicConfig(level=logging.INFO, format="[%(name)s] %(levelname)s %(message)s") | |
| model = env("VOYAGER_MODEL", "muse-spark-1.3-contributor-free") | |
| aux_model = env("VOYAGER_AUX_MODEL", "muse-spark-1.3-contributor-free") | |
| base_url = env("VOYAGER_OPENAI_BASE", "http://127.0.0.1:7864/v1") | |
| api_key = env("VOYAGER_API_KEY", "local-voyager") | |
| ckpt_dir = env("VOYAGER_CKPT_DIR", "/persist/minecraft-friends/voyager/ckpt-20261003-normal-v1") | |
| bot_username = env("MC_USERNAME", "Naldo_Bot") | |
| brain_profile = env("VOYAGER_BRAIN_PROFILE", "") | |
| if bot_username == "Naldo_Bot": | |
| try: | |
| from naldo_opencode import ( | |
| ALLOWED_MODELS as NALDO_ALLOWED_MODELS, | |
| BRAIN_PROFILE as NALDO_BRAIN_PROFILE, | |
| BRIDGE_BASE as NALDO_BRIDGE_BASE, | |
| is_allowed_model as _naldo_model_allowed, | |
| ) | |
| except Exception as exc: | |
| raise RuntimeError(f"Naldo OpenCode brain module unavailable: {exc}") from exc | |
| if ( | |
| brain_profile != NALDO_BRAIN_PROFILE | |
| or base_url != NALDO_BRIDGE_BASE | |
| or not _naldo_model_allowed(model) | |
| or not _naldo_model_allowed(aux_model) | |
| ): | |
| raise RuntimeError( | |
| f"Brain lock rejected {bot_username}: profile={brain_profile!r}, base={base_url!r}, " | |
| f"model={model!r}, aux={aux_model!r}; Naldo requires profile={NALDO_BRAIN_PROFILE!r}, " | |
| f"base={NALDO_BRIDGE_BASE!r}, models in {sorted(NALDO_ALLOWED_MODELS)} (opencode provider only)" | |
| ) | |
| expected = None | |
| elif bot_username == "Gemini_Bot": | |
| expected = ("gemini-cli", "http://127.0.0.1:7863/v1", "gemini-3.5-flash-lite", "gemini-3.5-flash-lite") | |
| else: | |
| expected = None | |
| if expected and (brain_profile, base_url, model, aux_model) != expected: | |
| raise RuntimeError( | |
| f"Brain lock rejected {bot_username}: profile={brain_profile!r}, base={base_url!r}, " | |
| f"model={model!r}, aux={aux_model!r}" | |
| ) | |
| ckpt = Path(ckpt_dir) | |
| required_resume_defaults = { | |
| ckpt / "action" / "chest_memory.json": {}, | |
| ckpt / "curriculum" / "completed_tasks.json": [], | |
| ckpt / "curriculum" / "failed_tasks.json": [], | |
| ckpt / "curriculum" / "qa_cache.json": {}, | |
| ckpt / "skill" / "skills.json": {}, | |
| } | |
| core_state = [ | |
| ckpt / "curriculum" / "completed_tasks.json", | |
| ckpt / "curriculum" / "failed_tasks.json", | |
| ckpt / "skill" / "skills.json", | |
| ] | |
| has_persisted_state = any(path.exists() for path in core_state) | |
| if has_persisted_state: | |
| skills_json = ckpt / "skill" / "skills.json" | |
| vector_dir = ckpt / "skill" / "vectordb" | |
| if not skills_json.exists() and vector_dir.exists(): | |
| shutil.rmtree(vector_dir, ignore_errors=True) | |
| print("[voyager] removed orphaned derived skill vectordb", flush=True) | |
| for path, default in required_resume_defaults.items(): | |
| if not path.exists(): | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(default, indent=2) + "\n", encoding="utf-8") | |
| print(f"[voyager] repaired missing checkpoint file: {path.name}", flush=True) | |
| required_resume_files = list(required_resume_defaults) | |
| resume = all(path.exists() for path in required_resume_files) | |
| # Keep long-lived curriculum memory compact and consistent. Old retries can | |
| # leave the same task in failed_tasks many times, including tasks that were | |
| # later completed under a spelling variant such as copper ore/copper_ore. | |
| if resume: | |
| def _norm_task(value: str): | |
| return task_spec(value) or " ".join(str(value).lower().replace("_", " ").split()) | |
| completed_path = ckpt / "curriculum" / "completed_tasks.json" | |
| failed_path = ckpt / "curriculum" / "failed_tasks.json" | |
| try: | |
| original_completed = json.loads(completed_path.read_text(encoding="utf-8")) | |
| original_failed = json.loads(failed_path.read_text(encoding="utf-8")) | |
| archive = ckpt / "quarantine" / "curriculum_history_before_semantic_compaction_v1.json" | |
| if not archive.exists(): | |
| archive.parent.mkdir(parents=True, exist_ok=True) | |
| archive.write_text(json.dumps({"completed": original_completed, "failed": original_failed}, indent=2) + "\n", encoding="utf-8") | |
| def unique(values): | |
| seen, result = set(), [] | |
| for value in values: | |
| key = _norm_task(value) | |
| if key not in seen: | |
| seen.add(key) | |
| result.append(str(value)) | |
| return result | |
| completed = unique(original_completed) | |
| completed_norm = {_norm_task(item) for item in completed} | |
| failed = [item for item in unique(original_failed) if _norm_task(item) not in completed_norm] | |
| # One-time repair for failures recorded while the Creative helper | |
| # context file was missing. Those attempts never reached the model | |
| # or Minecraft action, so they are not genuine task failures. | |
| repair_marker = ckpt / ".creative_context_repair_v1" | |
| if not repair_marker.exists(): | |
| failed = [item for item in failed if _norm_task(item) != "mine 1 wood log"] | |
| repair_marker.write_text("ok\n", encoding="utf-8") | |
| print("[voyager] removed false starter-task failure from missing Creative context", flush=True) | |
| # Granite called placeBlocks("brick") / ("brick_block") for the | |
| # legitimate "Place 1 bricks" task. Both failed before changing | |
| # the world. Archive the failure before allowing one fresh attempt | |
| # with the exact task ID bound by the Action parser. | |
| bricks_marker = ckpt / ".bricks_block_id_repair_v1" | |
| if bot_username == "Naldo_Bot" and not bricks_marker.exists(): | |
| if "Place 1 bricks" in failed and "Place 1 bricks" not in completed: | |
| bricks_archive = ckpt / "quarantine" / "bricks_block_id_failure_v1.json" | |
| bricks_archive.parent.mkdir(parents=True, exist_ok=True) | |
| bricks_archive.write_text(json.dumps({"failed": original_failed}, indent=2) + "\n", encoding="utf-8") | |
| failed = [item for item in failed if item != "Place 1 bricks"] | |
| print("[voyager] archived invalid brick-ID failure for one retry", flush=True) | |
| bricks_marker.write_text("ok\n", encoding="utf-8") | |
| # The Babel bridge briefly rejected a Python len() call on its | |
| # JavaScript Proxy, so these two logged tasks never executed. | |
| # Keep the original failure records before allowing one retry. | |
| proxy_marker = ckpt / ".action_proxy_length_repair_v1" | |
| if not proxy_marker.exists(): | |
| retryable = { | |
| "Naldo_Bot": {"Place 2 red_concrete"}, | |
| "Gemini_Bot": {"Place 1 emerald_block"}, | |
| }.get(bot_username, set()) | |
| removed = [item for item in failed if item in retryable and item not in completed] | |
| if removed: | |
| proxy_archive = ckpt / "quarantine" / "action_proxy_length_failures_v1.json" | |
| proxy_archive.parent.mkdir(parents=True, exist_ok=True) | |
| proxy_archive.write_text(json.dumps({"failed": original_failed, "retryable": removed}, indent=2) + "\n", encoding="utf-8") | |
| failed = [item for item in failed if item not in removed] | |
| print(f"[voyager] archived Action Proxy parser failures: {', '.join(removed)}", flush=True) | |
| proxy_marker.write_text("ok\n", encoding="utf-8") | |
| for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)): | |
| if values != original: | |
| temporary = path.with_suffix(".json.tmp") | |
| temporary.write_text(json.dumps(values, indent=2) + "\n", encoding="utf-8") | |
| temporary.replace(path) | |
| except Exception as exc: | |
| print(f"[voyager] checkpoint curriculum cleanup skipped: {exc}", flush=True) | |
| refresh_learning_state(ckpt, bot_username) | |
| start_learning_watcher(ckpt, bot_username) | |
| os.environ["OPENAI_API_BASE"] = base_url | |
| os.environ["OPENAI_API_KEY"] = api_key | |
| print(f"[voyager] upstream={VOYAGER_HOME}", flush=True) | |
| print(f"[voyager] openai_base={base_url}", flush=True) | |
| print(f"[voyager] minecraft=127.0.0.1:{env('MC_PORT', '25565')}", flush=True) | |
| print(f"[voyager] model={model}", flush=True) | |
| print(f"[voyager] auxiliary_roles_model={aux_model}", flush=True) | |
| print(f"[voyager] bot_username={bot_username}", flush=True) | |
| print(f"[voyager] brain_profile={brain_profile}", flush=True) | |
| print(f"[voyager] checkpoint={ckpt_dir} resume={resume}", flush=True) | |
| voyager = Voyager( | |
| mc_port=int(env("MC_PORT", "25565")), | |
| openai_api_key=api_key, | |
| server_port=int(env("MINEFLAYER_API_PORT", "3000")), | |
| env_wait_ticks=int(env("ENV_WAIT_TICKS", "20")), | |
| env_request_timeout=int(env("ENV_REQUEST_TIMEOUT", "600")), | |
| max_iterations=int(env("MAX_ITERATIONS", "160")), | |
| action_agent_model_name=model, | |
| action_agent_temperature=0, | |
| action_agent_task_max_retries=4, | |
| action_agent_show_chat_log=True, | |
| curriculum_agent_model_name=model, | |
| curriculum_agent_temperature=0, | |
| curriculum_agent_qa_model_name=aux_model, | |
| curriculum_agent_qa_temperature=0, | |
| critic_agent_model_name=model, | |
| critic_agent_temperature=0, | |
| skill_manager_model_name=aux_model, | |
| skill_manager_temperature=0, | |
| skill_manager_retrieval_top_k=5, | |
| openai_api_request_timeout=int(env("OPENAI_API_REQUEST_TIMEOUT", "240")), | |
| ckpt_dir=ckpt_dir, | |
| resume=resume, | |
| ) | |
| attestation = attest_original_runtime(voyager, VOYAGER_HOME) | |
| attach_role_metrics(voyager, bot_username) | |
| voyager.critic_agent = VerifiedCreativeCritic(voyager.critic_agent) | |
| write_attestation(env("VOYAGER_ATTESTATION_FILE", ""), attestation, bot_username) | |
| print( | |
| f"[voyager] verified original engine {attestation['upstream_commit']} for " | |
| f"{bot_username}; decision methods attested: " | |
| f"{', '.join(attestation['decision_methods_verified'])}; official Critic with " | |
| f"independent world evidence", | |
| flush=True, | |
| ) | |
| # Remove the known stale Creative-mode mining skill produced before the | |
| # mineBlock postcondition was fixed. In Creative, breaking copper ore does | |
| # not imply a copper_ingot drop, so that cached skill teaches a false check. | |
| stale = voyager.skill_manager.skills.get("mineCopperOre") | |
| stale_code = str(stale.get("code", "")) if isinstance(stale, dict) else "" | |
| if stale and ("copper_ingot" in stale_code or "raw_copper" in stale_code): | |
| try: | |
| voyager.skill_manager.vectordb._collection.delete(ids=["mineCopperOre"]) | |
| except Exception: | |
| pass | |
| voyager.skill_manager.skills.pop("mineCopperOre", None) | |
| skills_path = ckpt / "skill" / "skills.json" | |
| skills_path.write_text( | |
| json.dumps(voyager.skill_manager.skills, indent=2) + "\n", | |
| encoding="utf-8", | |
| ) | |
| for folder, suffix in (("code", "*.js"), ("description", "*.txt")): | |
| for path in (ckpt / "skill" / folder).glob("mineCopperOre" + suffix[1:]): | |
| try: | |
| path.unlink() | |
| except OSError: | |
| pass | |
| try: | |
| voyager.skill_manager.vectordb.persist() | |
| except Exception: | |
| pass | |
| print("[voyager] removed stale Creative mineCopperOre skill", flush=True) | |
| def _progress_fingerprint(directory: Path) -> str: | |
| # Process-alive is not progress: readiness stayed true while position | |
| # froze and no model calls advanced. Fingerprint the checkpoint that | |
| # only moves when tasks complete/fail plus the newest curriculum file | |
| # mtime, so a wedged bridge/Mineflayer is distinguishable from slow | |
| # inference. | |
| try: | |
| completed = (directory / "curriculum" / "completed_tasks.json").read_text(encoding="utf-8") | |
| except OSError: | |
| completed = "" | |
| try: | |
| failed = (directory / "curriculum" / "failed_tasks.json").read_text(encoding="utf-8") | |
| except OSError: | |
| failed = "" | |
| mtimes: list[str] = [] | |
| for relative in ("curriculum/completed_tasks.json", "curriculum/failed_tasks.json", "skill/skills.json"): | |
| try: | |
| mtimes.append(str((directory / relative).stat().st_mtime_ns)) | |
| except OSError: | |
| mtimes.append("-") | |
| return f"{len(completed)}:{len(failed)}:{'|'.join(mtimes)}" | |
| stall_seconds = int(float(env("VOYAGER_STALL_SECONDS", "1800"))) | |
| _stall_state: dict[str, object] = {"fingerprint": _progress_fingerprint(ckpt), "at": time.monotonic()} | |
| def _stall_watch() -> None: | |
| while True: | |
| time.sleep(60) | |
| try: | |
| current = _progress_fingerprint(ckpt) | |
| except Exception: | |
| continue | |
| now = time.monotonic() | |
| if current != _stall_state["fingerprint"]: | |
| _stall_state["fingerprint"] = current | |
| _stall_state["at"] = now | |
| continue | |
| if now - float(_stall_state["at"]) >= stall_seconds: # type: ignore[arg-type] | |
| print( | |
| f"[voyager] STALL WATCHDOG: no checkpoint progress for {stall_seconds}s; " | |
| "exiting so the stack watchdog can hard-reset Mineflayer and resume", | |
| flush=True, | |
| ) | |
| os._exit(75) | |
| threading.Thread(target=_stall_watch, daemon=True, name="voyager-stall-watch").start() | |
| # Supported upstream option: retain the live connection and observe every step. | |
| # The existing disconnect recovery and process watchdog still reconnect on failure. | |
| try: | |
| quarantine_invalid_skills(voyager, ckpt) | |
| quarantine_template_skills(voyager, ckpt) | |
| voyager.learn(reset_env=False) | |
| finally: | |
| # Upstream's non-daemon log-monitor thread keeps Python alive after a | |
| # fatal curriculum error. Stop only this runner's Mineflayer child so | |
| # the existing watchdog can observe exit and resume the checkpoint. | |
| process = voyager.env.mineflayer.process | |
| if process is not None and process.poll() is None: | |
| try: | |
| process.terminate() | |
| process.wait(timeout=10) | |
| except Exception: | |
| process.kill() | |
| process.wait(timeout=5) | |
| if __name__ == "__main__": | |
| main() | |