naldo commited on
Commit
ef1d4f0
·
1 Parent(s): d17cab1

Bind Creative placement to task IDs and retain Gemini failure diagnostics

Browse files
README.md CHANGED
@@ -27,7 +27,7 @@ Endpoint OpenAI-compatible dedicado ao projeto original MineDojo/Voyager.
27
  - Alias principal: `granite-4.0-h-tiny`
28
  - CPU: 16 threads de decode e 16 threads de prefill/batch
29
 
30
- Action, Curriculum, Critic e Skill Manager usam o mesmo Granite H-Tiny. O
31
  Curriculum em Creative usa contexto determinístico em vez de consultar QA
32
  Survival para cada tarefa, reduzindo latência e evitando receitas/pré-requisitos
33
  incompatíveis com o modo Creative.
@@ -52,9 +52,9 @@ O Voyager original usa embeddings no SkillManager e no cache de QA do Curriculum
52
  Referência:
53
  https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF
54
 
55
- ## CPU-only estrito
56
 
57
- A inferência não usa GPU:
58
 
59
  - `CUDA_VISIBLE_DEVICES=""`
60
  - `HIP_VISIBLE_DEVICES=""`
@@ -73,16 +73,15 @@ a função não é chamada pela API.
73
 
74
  ## Compatibilidade Voyager
75
 
76
- Por padrão `PLAYER_MODE=0`. Portanto o servidor não injeta o antigo prompt Bedrock e
77
- não força JSON. As adições aos prompts exigem inventário real, contagem por `item.count`,
78
- aquisição/crafting dos pré-requisitos e verificação do resultado observado.
 
79
 
80
  O endpoint aceita temperatura 0.0, que coincide com os defaults do Voyager para
81
  ActionAgent, CurriculumAgent, CriticAgent e SkillManager.
82
 
83
- Para clientes OpenAI que usam `tools`, a camada HTTP converte o formato XML nativo do
84
- Qwen3-Coder (`<tool_call>`, `<function=...>`, `<parameter=...>`) para `tool_calls` no
85
- formato OpenAI. Isso não altera os prompts nem o loop do Voyager.
86
 
87
  ## API
88
 
@@ -92,22 +91,23 @@ formato OpenAI. Isso não altera os prompts nem o loop do Voyager.
92
  - `GET /v1/models`
93
  - `POST /v1/chat/completions`
94
  - `POST /v1/embeddings`
 
 
95
  - `GET /gemini-bot/status`
96
 
97
  ## Segundo jogador: Gemini_Bot
98
 
99
- `Gemini_Bot` entra no mesmo servidor Java 1.19 em Creative, com processo,
100
- reconexão e estado independentes do Voyager/Naldo_Bot. A cada turno autônomo
101
- ele observa posição, jogadores próximos e blocos visíveis. O ADM
102
- `IffyBinkie6134` também pode escrever `Gemini_Bot, segue o Naldo` no chat.
103
- O bot aceita uma decisão JSON do Gemini CLI e executa somente ações pequenas
104
- e validadas: explorar, seguir, minerar um bloco próximo, colocar um bloco,
105
- responder no chat ou aguardar. Mineração e colocação verificam o bloco real.
106
 
107
  - CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
108
  - Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
109
  - Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
110
- - Status: `/gemini-bot/status` (conexão, decisões e contadores; sem chaves).
 
111
  - Controle de inicialização: `GEMINI_BOT_AUTOSTART=0` desativa somente o
112
  segundo bot. O Naldo_Bot e o mundo persistente seguem independentes.
113
 
@@ -129,8 +129,12 @@ O comportamento do agente deve continuar vindo do projeto original:
129
  https://github.com/MineDojo/Voyager
130
 
131
  Este Space executa o Voyager fixado em `55e45a880755d0c8c66ca7fb5fe7962ac8974f89`
132
- com correções de reconexão, observações nulas, movimento e mineração limitada.
133
- Mundo, autenticação e checkpoint permanecem em `/persist/minecraft-friends`.
 
 
 
 
134
 
135
  Validação offline, sem iniciar modelos ou servidor de jogo:
136
  `python tests/test_voyager_patch.py /caminho/para/Voyager-oficial`
 
27
  - Alias principal: `granite-4.0-h-tiny`
28
  - CPU: 16 threads de decode e 16 threads de prefill/batch
29
 
30
+ No Naldo_Bot, Action, Curriculum, Critic e Skill Manager usam o Granite H-Tiny. O
31
  Curriculum em Creative usa contexto determinístico em vez de consultar QA
32
  Survival para cada tarefa, reduzindo latência e evitando receitas/pré-requisitos
33
  incompatíveis com o modo Creative.
 
52
  Referência:
53
  https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF
54
 
55
+ ## Inferência local somente em CPU
56
 
57
+ A inferência Granite e os embeddings locais não usam GPU:
58
 
59
  - `CUDA_VISIBLE_DEVICES=""`
60
  - `HIP_VISIBLE_DEVICES=""`
 
73
 
74
  ## Compatibilidade Voyager
75
 
76
+ Por padrão `PLAYER_MODE=0`. O servidor não injeta o antigo prompt Bedrock.
77
+ Os dois Voyagers jogam em Creative: mineração, colocação, equipamento e inventário
78
+ são verificados a partir da observação real. O `placeBlocks` recebe o ID exato
79
+ da tarefa canônica; o agente não pode substituir `bricks` por `brick`.
80
 
81
  O endpoint aceita temperatura 0.0, que coincide com os defaults do Voyager para
82
  ActionAgent, CurriculumAgent, CriticAgent e SkillManager.
83
 
84
+ O endpoint mantém a compatibilidade HTTP de chat e embeddings para os clientes.
 
 
85
 
86
  ## API
87
 
 
91
  - `GET /v1/models`
92
  - `POST /v1/chat/completions`
93
  - `POST /v1/embeddings`
94
+ - `GET /minecraft/status`
95
+ - `GET /voyager/status`
96
  - `GET /gemini-bot/status`
97
 
98
  ## Segundo jogador: Gemini_Bot
99
 
100
+ `Gemini_Bot` executa uma segunda instância completa do Voyager no mesmo servidor
101
+ Java 1.19, com processo Mineflayer e checkpoint próprios. Curriculum, Action,
102
+ Critic e Skill Manager consultam o Gemini CLI por um proxy local exclusivo;
103
+ somente os embeddings usam o modelo local. `Naldo_Bot` continua usando Granite.
104
+ Ambos validam tarefas Creative pela mudança real no mundo ou pelo inventário.
 
 
105
 
106
  - CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
107
  - Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
108
  - Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
109
+ - Status: `/gemini-bot/status` (conexão, tarefas, métricas e falhas por posição
110
+ da chave; nunca revela valores de chaves).
111
  - Controle de inicialização: `GEMINI_BOT_AUTOSTART=0` desativa somente o
112
  segundo bot. O Naldo_Bot e o mundo persistente seguem independentes.
113
 
 
129
  https://github.com/MineDojo/Voyager
130
 
131
  Este Space executa o Voyager fixado em `55e45a880755d0c8c66ca7fb5fe7962ac8974f89`
132
+ com correções de reconexão, observações nulas, exploração horizontal limitada e
133
+ mineração que não cava o chão sob o jogador. O mundo plano tem superfície em Y=64:
134
+ `/persist/minecraft-friends/world-20260927-flat-y64-v1`. Os checkpoints atuais
135
+ são `/persist/minecraft-friends/voyager/ckpt-20260927-flat-y64-v1` e
136
+ `/persist/minecraft-friends/gemini-voyager/ckpt-20260927-flat-y64-v1`.
137
+ A autenticação Xbox/Friends também persiste sob `/persist/minecraft-friends`.
138
 
139
  Validação offline, sem iniciar modelos ou servidor de jogo:
140
  `python tests/test_voyager_patch.py /caminho/para/Voyager-oficial`
gemini_voyager_proxy.py CHANGED
@@ -27,7 +27,9 @@ EMBED_BASE = os.environ.get("GEMINI_VOYAGER_EMBED_BASE", "http://127.0.0.1:7860/
27
  LOCK = threading.Lock()
28
  STATE_LOCK = threading.Lock()
29
  state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
30
- "prompt_tokens": 0, "completion_tokens": 0, "last_error": None}
 
 
31
  api = FastAPI(title="Gemini Voyager Adapter", version="1.0.0")
32
  def _content(value: Any) -> str:
33
  if isinstance(value, str):
@@ -108,11 +110,16 @@ def _call_cli(prompt: str) -> tuple[str, int, int]:
108
  state["last_error"] = None
109
  return envelope["response"], p_tokens, c_tokens
110
  code = envelope.get("error", {}).get("code") if isinstance(envelope.get("error"), dict) else None
111
- last = f"Gemini CLI exit {proc.returncode}" + (f" API {code}" if code is not None else "")
 
112
  with STATE_LOCK:
113
  state["selected_key"] = (slot + 1) % len(KEYS)
114
  state["failures"] += 1
115
  state["last_error"] = last
 
 
 
 
116
  raise RuntimeError(last)
117
 
118
 
 
27
  LOCK = threading.Lock()
28
  STATE_LOCK = threading.Lock()
29
  state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
30
+ "prompt_tokens": 0, "completion_tokens": 0, "last_error": None,
31
+ "last_failure": None, "last_failure_at": None,
32
+ "last_failed_key_slot": None, "failures_by_key_slot": [0] * len(KEYS)}
33
  api = FastAPI(title="Gemini Voyager Adapter", version="1.0.0")
34
  def _content(value: Any) -> str:
35
  if isinstance(value, str):
 
110
  state["last_error"] = None
111
  return envelope["response"], p_tokens, c_tokens
112
  code = envelope.get("error", {}).get("code") if isinstance(envelope.get("error"), dict) else None
113
+ safe_code = str(code) if isinstance(code, (str, int)) and str(code).isalnum() and len(str(code)) <= 32 else None
114
+ last = f"Gemini CLI exit {proc.returncode}" + (f" API {safe_code}" if safe_code else "")
115
  with STATE_LOCK:
116
  state["selected_key"] = (slot + 1) % len(KEYS)
117
  state["failures"] += 1
118
  state["last_error"] = last
119
+ state["last_failure"] = last
120
+ state["last_failure_at"] = int(time.time())
121
+ state["last_failed_key_slot"] = slot + 1
122
+ state["failures_by_key_slot"][slot] += 1
123
  raise RuntimeError(last)
124
 
125
 
tests/test_gemini_bot_stack.py CHANGED
@@ -1,9 +1,14 @@
1
  """Gemini_Bot runs the same Voyager architecture with isolated state and credentials."""
2
  import ast
 
3
  import os
4
  from pathlib import Path
 
5
  import sys
6
  from tempfile import TemporaryDirectory
 
 
 
7
  from unittest.mock import patch
8
 
9
  ROOT = Path(__file__).resolve().parents[1]
@@ -40,7 +45,34 @@ def test_no_stored_credentials():
40
  assert tree is not None
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  if __name__ == "__main__":
44
  test_isolation()
45
  test_no_stored_credentials()
 
46
  print("Gemini bot stack tests passed")
 
1
  """Gemini_Bot runs the same Voyager architecture with isolated state and credentials."""
2
  import ast
3
+ import json
4
  import os
5
  from pathlib import Path
6
+ import subprocess
7
  import sys
8
  from tempfile import TemporaryDirectory
9
+ import threading
10
+ import time
11
+ from types import SimpleNamespace
12
  from unittest.mock import patch
13
 
14
  ROOT = Path(__file__).resolve().parents[1]
 
45
  assert tree is not None
46
 
47
 
48
+ def test_proxy_preserves_safe_failure_diagnostics():
49
+ source = ast.parse((ROOT / "gemini_voyager_proxy.py").read_text())
50
+ func = next(node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == "_call_cli")
51
+ state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
52
+ "prompt_tokens": 0, "completion_tokens": 0, "last_error": None,
53
+ "last_failure": None, "last_failure_at": None, "last_failed_key_slot": None,
54
+ "failures_by_key_slot": [0, 0]}
55
+ ns = {"KEYS": ["private-1", "private-2"], "LOCK": threading.Lock(),
56
+ "STATE_LOCK": threading.Lock(), "state": state, "os": os, "subprocess": subprocess,
57
+ "json": json, "time": time, "CLI": "gemini", "MODEL": "gemini-3.5-flash-lite",
58
+ "WORKSPACE": "/tmp", "HOME": "/tmp", "SYSTEM_MD": "/tmp/system.md",
59
+ "_usage": lambda stats: (12, 3)}
60
+ exec(compile(ast.Module(body=[func], type_ignores=[]), "proxy.py", "exec"), ns)
61
+ responses = [SimpleNamespace(returncode=1, stdout='{"error":{"code":429}}'),
62
+ SimpleNamespace(returncode=0, stdout='{"response":"ok","stats":{}}')]
63
+ with patch.object(subprocess, "run", side_effect=responses) as run:
64
+ assert ns["_call_cli"]("hello") == ("ok", 12, 3)
65
+ assert run.call_args_list[0].kwargs["env"]["GEMINI_API_KEY"] == "private-1"
66
+ assert run.call_args_list[1].kwargs["env"]["GEMINI_API_KEY"] == "private-2"
67
+ assert state["failures"] == 1 and state["last_error"] is None
68
+ assert state["last_failure"] == "Gemini CLI exit 1 API 429"
69
+ assert state["last_failed_key_slot"] == 1
70
+ assert state["failures_by_key_slot"] == [1, 0]
71
+ assert "private-" not in str(state)
72
+
73
+
74
  if __name__ == "__main__":
75
  test_isolation()
76
  test_no_stored_credentials()
77
+ test_proxy_preserves_safe_failure_diagnostics()
78
  print("Gemini bot stack tests passed")
tests/test_voyager_patch.py CHANGED
@@ -27,6 +27,7 @@ from voyager_verification import (
27
  verify_task, quarantine_invalid_skills, task_history,
28
  )
29
  from voyager_action_parse import extract_action_code
 
30
 
31
 
32
  def functions(filename, names, namespace):
@@ -86,7 +87,7 @@ def test_resume():
86
  file.parent.mkdir(parents=True, exist_ok=True)
87
  file.write_text("{}")
88
  original_completed = ["Mine 1 stone block to upgrade tools", "Mine 1 stone", "Place 4 snow_block"]
89
- original_failed = ["Mine 1 stone", "Craft a stone pickaxe", "Craft a stone pickaxe"]
90
  (ckpt / "curriculum/completed_tasks.json").write_text(json.dumps(original_completed))
91
  (ckpt / "curriculum/failed_tasks.json").write_text(json.dumps(original_failed))
92
  voyager = Mock()
@@ -132,6 +133,7 @@ def test_resume():
132
  voyager.return_value.env.mineflayer.process.terminate.assert_called_once_with()
133
  assert json.loads((ckpt / "curriculum/completed_tasks.json").read_text()) == original_completed[::2]
134
  assert json.loads((ckpt / "curriculum/failed_tasks.json").read_text()) == ["Craft a stone pickaxe"]
 
135
  archive = json.loads((ckpt / "quarantine/curriculum_history_before_semantic_compaction_v1.json").read_text())
136
  assert archive == {"completed": original_completed, "failed": original_failed}
137
  voyager.return_value.learn.side_effect = RuntimeError("fatal curriculum error")
@@ -239,15 +241,17 @@ def check_action_behavior(source):
239
  def parse_js(code):
240
  subprocess.run(["node", "--check", "-"], input=code, text=True,
241
  capture_output=True, check=True)
242
- node = Node(type="FunctionDeclaration", id=SimpleNamespace(name="placeStone"),
 
243
  params=[SimpleNamespace(type="Identifier", name="bot")])
244
  node["async"] = True
 
245
  return SimpleNamespace(program=SimpleNamespace(body=[node]))
246
 
247
- generator = lambda node: SimpleNamespace(code="async function placeStone(bot) { await placeBlocks(bot, 'stone', 1); }")
248
  require = lambda package: (SimpleNamespace(parseSync=parse_js) if package == "@babel/core"
249
  else SimpleNamespace(default=generator))
250
- ns = {"extract_action_code": extract_action_code, "require": require}
251
  exec(compile(ast.Module(body=[method], type_ignores=[]), "action.py", "exec"), ns)
252
  process = MethodType(ns["process_ai_message"], SimpleNamespace())
253
  samples = [
@@ -271,6 +275,12 @@ def check_action_behavior(source):
271
  assert "await placeStone(bot)" not in extract_action_code(content)
272
  else:
273
  assert "before program execution" in result
 
 
 
 
 
 
274
  print("PASS Action parser normalizes prose, rejects ambiguity, truncation and extra functions")
275
 
276
 
@@ -415,6 +425,7 @@ const bot = {entity:{position:{x:0.5,y:64,z:0.5}},
415
  assert "Blocked invalid movement packet" in index
416
  core = (stack.SRC / "voyager/voyager.py").read_text()
417
  assert "Recovered Voyager observation without reconnecting Mineflayer" in core
 
418
  assert "traceback.print_exc()" in core
419
 
420
 
 
27
  verify_task, quarantine_invalid_skills, task_history,
28
  )
29
  from voyager_action_parse import extract_action_code
30
+ from voyager_action_parse import canonical_place_code
31
 
32
 
33
  def functions(filename, names, namespace):
 
87
  file.parent.mkdir(parents=True, exist_ok=True)
88
  file.write_text("{}")
89
  original_completed = ["Mine 1 stone block to upgrade tools", "Mine 1 stone", "Place 4 snow_block"]
90
+ original_failed = ["Mine 1 stone", "Craft a stone pickaxe", "Craft a stone pickaxe", "Place 1 bricks"]
91
  (ckpt / "curriculum/completed_tasks.json").write_text(json.dumps(original_completed))
92
  (ckpt / "curriculum/failed_tasks.json").write_text(json.dumps(original_failed))
93
  voyager = Mock()
 
133
  voyager.return_value.env.mineflayer.process.terminate.assert_called_once_with()
134
  assert json.loads((ckpt / "curriculum/completed_tasks.json").read_text()) == original_completed[::2]
135
  assert json.loads((ckpt / "curriculum/failed_tasks.json").read_text()) == ["Craft a stone pickaxe"]
136
+ assert json.loads((ckpt / "quarantine/bricks_block_id_failure_v1.json").read_text()) == {"failed": original_failed}
137
  archive = json.loads((ckpt / "quarantine/curriculum_history_before_semantic_compaction_v1.json").read_text())
138
  assert archive == {"completed": original_completed, "failed": original_failed}
139
  voyager.return_value.learn.side_effect = RuntimeError("fatal curriculum error")
 
241
  def parse_js(code):
242
  subprocess.run(["node", "--check", "-"], input=code, text=True,
243
  capture_output=True, check=True)
244
+ name = code.split("function ", 1)[1].split("(", 1)[0]
245
+ node = Node(type="FunctionDeclaration", id=SimpleNamespace(name=name),
246
  params=[SimpleNamespace(type="Identifier", name="bot")])
247
  node["async"] = True
248
+ node["source"] = code
249
  return SimpleNamespace(program=SimpleNamespace(body=[node]))
250
 
251
+ generator = lambda node: SimpleNamespace(code=node["source"])
252
  require = lambda package: (SimpleNamespace(parseSync=parse_js) if package == "@babel/core"
253
  else SimpleNamespace(default=generator))
254
+ ns = {"extract_action_code": extract_action_code, "canonical_place_code": canonical_place_code, "require": require}
255
  exec(compile(ast.Module(body=[method], type_ignores=[]), "action.py", "exec"), ns)
256
  process = MethodType(ns["process_ai_message"], SimpleNamespace())
257
  samples = [
 
275
  assert "await placeStone(bot)" not in extract_action_code(content)
276
  else:
277
  assert "before program execution" in result
278
+ wrong_id = "```js\nasync function placeBrick(bot) { await placeBlocks(bot, 'brick_block', 4); }\n```"
279
+ repaired = process(SimpleNamespace(content=wrong_id), task="Place 1 bricks")
280
+ assert repaired["program_name"] == "placeBrick"
281
+ assert 'placeBlocks(bot, "bricks", 1)' in repaired["program_code"]
282
+ assert "brick_block" not in repaired["program_code"]
283
+ assert "brick_block" in process(SimpleNamespace(content=wrong_id), task="Mine 1 stone")["program_code"]
284
  print("PASS Action parser normalizes prose, rejects ambiguity, truncation and extra functions")
285
 
286
 
 
425
  assert "Blocked invalid movement packet" in index
426
  core = (stack.SRC / "voyager/voyager.py").read_text()
427
  assert "Recovered Voyager observation without reconnecting Mineflayer" in core
428
+ assert "process_ai_message(message=ai_message, task=self.task)" in core
429
  assert "traceback.print_exc()" in core
430
 
431
 
voyager_action_parse.py CHANGED
@@ -1,7 +1,10 @@
1
  """Extract one executable Action function from small-model responses."""
2
 
 
3
  import re
4
 
 
 
5
 
6
  _FENCE = re.compile(r"```([^\n`]*)\n(.*?)```", re.DOTALL)
7
  _FUNCTION = re.compile(
@@ -79,3 +82,19 @@ def extract_action_code(response: str) -> str:
79
  if len(functions) != 1:
80
  raise ValueError(f"Expected exactly one complete Action function; found {len(functions)}")
81
  return functions[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Extract one executable Action function from small-model responses."""
2
 
3
+ import json
4
  import re
5
 
6
+ from voyager_verification import SAFE_PLACE_BLOCKS, canonical_task, task_spec
7
+
8
 
9
  _FENCE = re.compile(r"```([^\n`]*)\n(.*?)```", re.DOTALL)
10
  _FUNCTION = re.compile(
 
82
  if len(functions) != 1:
83
  raise ValueError(f"Expected exactly one complete Action function; found {len(functions)}")
84
  return functions[0]
85
+
86
+
87
+ def canonical_place_code(task: str, function_name: str) -> str | None:
88
+ """Bind a Creative placement to the verified task, not a guessed block ID."""
89
+ if canonical_task(task) != task:
90
+ return None
91
+ spec = task_spec(task)
92
+ if not spec or spec[0] != "place" or spec[2] not in SAFE_PLACE_BLOCKS:
93
+ return None
94
+ if not re.fullmatch(r"[A-Za-z_$][\w$]*", function_name):
95
+ raise ValueError("Invalid Action function name")
96
+ _, count, name = spec
97
+ return (
98
+ f"async function {function_name}(bot) {{ "
99
+ f"await placeBlocks(bot, {json.dumps(name)}, {count}); }}"
100
+ )
voyager_runner.py CHANGED
@@ -107,6 +107,20 @@ def main() -> None:
107
  repair_marker.write_text("ok\n", encoding="utf-8")
108
  print("[voyager] removed false starter-task failure from missing Creative context", flush=True)
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)):
111
  if values != original:
112
  temporary = path.with_suffix(".json.tmp")
 
107
  repair_marker.write_text("ok\n", encoding="utf-8")
108
  print("[voyager] removed false starter-task failure from missing Creative context", flush=True)
109
 
110
+ # Granite called placeBlocks("brick") / ("brick_block") for the
111
+ # legitimate "Place 1 bricks" task. Both failed before changing
112
+ # the world. Archive the failure before allowing one fresh attempt
113
+ # with the exact task ID bound by the Action parser.
114
+ bricks_marker = ckpt / ".bricks_block_id_repair_v1"
115
+ if bot_username == "Naldo_Bot" and not bricks_marker.exists():
116
+ if "Place 1 bricks" in failed and "Place 1 bricks" not in completed:
117
+ bricks_archive = ckpt / "quarantine" / "bricks_block_id_failure_v1.json"
118
+ bricks_archive.parent.mkdir(parents=True, exist_ok=True)
119
+ bricks_archive.write_text(json.dumps({"failed": original_failed}, indent=2) + "\n", encoding="utf-8")
120
+ failed = [item for item in failed if item != "Place 1 bricks"]
121
+ print("[voyager] archived invalid brick-ID failure for one retry", flush=True)
122
+ bricks_marker.write_text("ok\n", encoding="utf-8")
123
+
124
  for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)):
125
  if values != original:
126
  temporary = path.with_suffix(".json.tmp")
voyager_stack.py CHANGED
@@ -520,7 +520,7 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
520
  ' observation += f"Critique: {critique}\\n\\n"',
521
  ' observation += f"Critique: {critique[:1200]}\\n\\n"',
522
  )
523
- safe_action_parser = ''' def process_ai_message(self, message):
524
  # Do not concatenate fences or try the same malformed response 3 times.
525
  # The caller retries with fresh feedback while Mineflayer stays connected.
526
  try:
@@ -535,6 +535,12 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
535
  params = list(node["params"])
536
  if len(params) != 1 or params[0].type != "Identifier" or params[0].name != "bot":
537
  raise ValueError("Action function must take exactly one bot argument")
 
 
 
 
 
 
538
  return {
539
  "program_code": babel_generator(node).code,
540
  "program_name": node.id.name,
@@ -552,7 +558,7 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
552
  )
553
  if action_parser_count != 1:
554
  raise RuntimeError("Voyager Action parser marker changed")
555
- action_agent_text = 'from voyager_action_parse import extract_action_code\n' + action_agent_text
556
  action_agent_path.write_text(action_agent_text, encoding="utf-8")
557
 
558
  # Bound curriculum memory rendered into every task proposal. The full
@@ -901,16 +907,24 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
901
  # be parsed. EventRecorder immediately indexes events[0], so a truncated
902
  # LLM response turns a recoverable parse retry into IndexError. Record
903
  # the last valid observation instead and let Voyager retry the action.
904
- voyager_src = voyager_src.replace(
905
- ''' self.recorder.record([], self.task)
906
  print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
907
  ''',
908
  ''' if self.last_events:
909
  self.recorder.record(self.last_events, self.task)
910
  print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
911
  ''',
912
- 1,
913
- )
 
 
 
 
 
 
 
 
914
 
915
  old_recovery = ''' except Exception as e:\n time.sleep(3) # wait for mineflayer to exit\n info = {\n "task": task,\n "success": False,\n }\n # reset bot status here\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": self.last_events[-1][1]["inventory"],\n "equipment": self.last_events[-1][1]["status"]["equipment"],\n "position": self.last_events[-1][1]["status"]["position"],\n }\n )\n # use red color background to print the error\n print("Your last round rollout terminated due to error:")\n print(f"\\033[41m{e}\\033[0m")\n'''
916
  new_recovery = ''' except Exception as e:\n info = {\n "task": task,\n "success": False,\n }\n print("Your last round rollout terminated due to error:")\n traceback.print_exc()\n print(f"\\033[41m{type(e).__name__}: {e}\\033[0m")\n try:\n # Preserve the current Mineflayer connection whenever it is\n # still responsive. This prevents needless leave/rejoin.\n self.last_events = self.env.step("")\n print("Recovered Voyager observation without reconnecting Mineflayer")\n except Exception as recovery_error:\n print("Live recovery failed; performing one hard Mineflayer reset")\n traceback.print_exc()\n previous = self.last_events[-1][1] if self.last_events else {}\n status = previous.get("status", {}) if isinstance(previous, dict) else {}\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": previous.get("inventory", {}) if isinstance(previous, dict) else {},\n "equipment": status.get("equipment", []),\n "position": status.get("position", None),\n }\n )\n'''
 
520
  ' observation += f"Critique: {critique}\\n\\n"',
521
  ' observation += f"Critique: {critique[:1200]}\\n\\n"',
522
  )
523
+ safe_action_parser = ''' def process_ai_message(self, message, task=None):
524
  # Do not concatenate fences or try the same malformed response 3 times.
525
  # The caller retries with fresh feedback while Mineflayer stays connected.
526
  try:
 
535
  params = list(node["params"])
536
  if len(params) != 1 or params[0].type != "Identifier" or params[0].name != "bot":
537
  raise ValueError("Action function must take exactly one bot argument")
538
+ # A valid Place task supplies its exact Minecraft block ID. Small
539
+ # models sometimes change "bricks" into the non-block "brick";
540
+ # retain the function name while binding the primitive to the task.
541
+ place_code = canonical_place_code(task, node.id.name) if task else None
542
+ if place_code:
543
+ node = babel.parseSync(place_code).program.body[0]
544
  return {
545
  "program_code": babel_generator(node).code,
546
  "program_name": node.id.name,
 
558
  )
559
  if action_parser_count != 1:
560
  raise RuntimeError("Voyager Action parser marker changed")
561
+ action_agent_text = 'from voyager_action_parse import extract_action_code, canonical_place_code\n' + action_agent_text
562
  action_agent_path.write_text(action_agent_text, encoding="utf-8")
563
 
564
  # Bound curriculum memory rendered into every task proposal. The full
 
907
  # be parsed. EventRecorder immediately indexes events[0], so a truncated
908
  # LLM response turns a recoverable parse retry into IndexError. Record
909
  # the last valid observation instead and let Voyager retry the action.
910
+ voyager_src = voyager_src.replace(
911
+ ''' self.recorder.record([], self.task)
912
  print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
913
  ''',
914
  ''' if self.last_events:
915
  self.recorder.record(self.last_events, self.task)
916
  print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
917
  ''',
918
+ 1,
919
+ )
920
+ action_call = ' parsed_result = self.action_agent.process_ai_message(message=ai_message)'
921
+ if voyager_src.count(action_call) != 1:
922
+ raise RuntimeError("Voyager Action call marker changed")
923
+ voyager_src = voyager_src.replace(
924
+ action_call,
925
+ ' parsed_result = self.action_agent.process_ai_message(message=ai_message, task=self.task)',
926
+ 1,
927
+ )
928
 
929
  old_recovery = ''' except Exception as e:\n time.sleep(3) # wait for mineflayer to exit\n info = {\n "task": task,\n "success": False,\n }\n # reset bot status here\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": self.last_events[-1][1]["inventory"],\n "equipment": self.last_events[-1][1]["status"]["equipment"],\n "position": self.last_events[-1][1]["status"]["position"],\n }\n )\n # use red color background to print the error\n print("Your last round rollout terminated due to error:")\n print(f"\\033[41m{e}\\033[0m")\n'''
930
  new_recovery = ''' except Exception as e:\n info = {\n "task": task,\n "success": False,\n }\n print("Your last round rollout terminated due to error:")\n traceback.print_exc()\n print(f"\\033[41m{type(e).__name__}: {e}\\033[0m")\n try:\n # Preserve the current Mineflayer connection whenever it is\n # still responsive. This prevents needless leave/rejoin.\n self.last_events = self.env.step("")\n print("Recovered Voyager observation without reconnecting Mineflayer")\n except Exception as recovery_error:\n print("Live recovery failed; performing one hard Mineflayer reset")\n traceback.print_exc()\n previous = self.last_events[-1][1] if self.last_events else {}\n status = previous.get("status", {}) if isinstance(previous, dict) else {}\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": previous.get("inventory", {}) if isinstance(previous, dict) else {},\n "equipment": status.get("equipment", []),\n "position": status.get("position", None),\n }\n )\n'''