Recognize wood log species in verified Naldo mining tasks
Browse filesThe initial Mine 1 wood log task rejected proven oak_log mining because wood_log was compared as a literal block ID. Resolve generic wood names to 1.19 log species and give the Creative critic the actual mining rules. Physical block evidence remains required; chat and inventory alone cannot prove mining. Three regression cases and 38 related checks passed.
- tests/test_creative_mine_evidence.py +101 -0
- voyager_creative_critic.py +14 -1
- voyager_verification.py +30 -3
tests/test_creative_mine_evidence.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A proven Creative mine without drops must pass; chat/give fakery must not.
|
| 2 |
+
|
| 3 |
+
Live case: Naldo mined oak_log (verified block change, inventory oak_log 1, no
|
| 4 |
+
drops in Creative) for "Mine 1 wood log", yet completed stayed 0: the generic
|
| 5 |
+
name matched nothing, so evidence read "Verified 0/1 distinct wood_log" and
|
| 6 |
+
both the model and the deny-only veto failed a real mine.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
import voyager_verification as verification
|
| 17 |
+
from voyager_creative_critic import CREATIVE_EVIDENCE_FACTS, VerifiedCreativeCritic
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _live_mine_events(log_name="oak_log"):
|
| 21 |
+
"""Final observation shaped like the live Naldo attempt."""
|
| 22 |
+
return [("observe", {
|
| 23 |
+
"inventory": {log_name: 1, "netherite_axe": 1},
|
| 24 |
+
"status": {
|
| 25 |
+
"position": {"x": 33.38, "y": 67.0, "z": -7.52},
|
| 26 |
+
"equipment": ["netherite_helmet", "netherite_chestplate",
|
| 27 |
+
"netherite_leggings", "netherite_boots",
|
| 28 |
+
"netherite_axe", None],
|
| 29 |
+
"health": 20.0,
|
| 30 |
+
"food": 20.0,
|
| 31 |
+
},
|
| 32 |
+
"verified_actions": [
|
| 33 |
+
{"kind": "mine", "name": log_name, "positions": [[33, 66, -7]]}
|
| 34 |
+
],
|
| 35 |
+
"step_start_position": {"x": 26.5, "y": 64.0, "z": -6.5},
|
| 36 |
+
})]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_proven_creative_mine_passes_without_drops():
|
| 40 |
+
# Every 1.19 log species satisfies the generic "wood log" objective.
|
| 41 |
+
for log_name in ("oak_log", "birch_log", "spruce_log", "jungle_log",
|
| 42 |
+
"acacia_log", "dark_oak_log", "mangrove_log"):
|
| 43 |
+
supported, evidence = verification.verify_task(
|
| 44 |
+
_live_mine_events(log_name), "Mine 1 wood log")
|
| 45 |
+
assert supported is True, f"{log_name}: {evidence}"
|
| 46 |
+
assert verification.mechanical_verdict(
|
| 47 |
+
_live_mine_events(log_name), "Mine 1 wood log") is True
|
| 48 |
+
# Stone is not wood: the alias must not become a wildcard.
|
| 49 |
+
supported, _ = verification.verify_task(
|
| 50 |
+
_live_mine_events("stone"), "Mine 1 wood log")
|
| 51 |
+
assert supported is False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_give_or_chat_alone_never_proves_a_mine():
|
| 55 |
+
# creativeGiveItem/chat fakery: inventory present, zero verified blocks.
|
| 56 |
+
events = [("observe", {
|
| 57 |
+
"inventory": {"oak_log": 1},
|
| 58 |
+
"status": {"position": {"x": 0.0, "y": 64.0, "z": 0.0}},
|
| 59 |
+
"verified_actions": [],
|
| 60 |
+
})]
|
| 61 |
+
supported, evidence = verification.verify_task(events, "Mine 1 wood log")
|
| 62 |
+
assert supported is False, evidence
|
| 63 |
+
assert verification.mechanical_verdict(events, "Mine 1 wood log") is False
|
| 64 |
+
# Obtaining is a different objective and still reads the inventory.
|
| 65 |
+
supported, _ = verification.verify_task(events, "Obtain 1 wood log")
|
| 66 |
+
assert supported is True
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_wrapper_passes_real_mine_and_vetoes_fakery():
|
| 70 |
+
class Upstream:
|
| 71 |
+
def __init__(self):
|
| 72 |
+
self.calls = []
|
| 73 |
+
|
| 74 |
+
def check_task_success(self, *, events, task, context, chest_observation, max_retries=5):
|
| 75 |
+
self.calls.append({"context": context})
|
| 76 |
+
return True, "model says success"
|
| 77 |
+
|
| 78 |
+
upstream = Upstream()
|
| 79 |
+
critic = VerifiedCreativeCritic(upstream)
|
| 80 |
+
success, _ = critic.check_task_success(
|
| 81 |
+
events=_live_mine_events(), task="Mine 1 wood log",
|
| 82 |
+
context="ctx", chest_observation={}, max_retries=5)
|
| 83 |
+
assert success is True
|
| 84 |
+
context = upstream.calls[0]["context"]
|
| 85 |
+
assert "Independent world observation" in context
|
| 86 |
+
assert "Verified 1/1" in context
|
| 87 |
+
assert "no item drops" in context
|
| 88 |
+
assert CREATIVE_EVIDENCE_FACTS in context
|
| 89 |
+
|
| 90 |
+
upstream = Upstream()
|
| 91 |
+
critic = VerifiedCreativeCritic(upstream)
|
| 92 |
+
fake = [("observe", {
|
| 93 |
+
"inventory": {"oak_log": 1},
|
| 94 |
+
"status": {"position": {"x": 0.0, "y": 64.0, "z": 0.0}},
|
| 95 |
+
"verified_actions": [],
|
| 96 |
+
})]
|
| 97 |
+
success, reason = critic.check_task_success(
|
| 98 |
+
events=fake, task="Mine 1 wood log",
|
| 99 |
+
context="ctx", chest_observation={}, max_retries=5)
|
| 100 |
+
assert success is False, "chat/give fakery must not complete a Mine task"
|
| 101 |
+
assert "contradicts" in reason
|
voyager_creative_critic.py
CHANGED
|
@@ -16,6 +16,19 @@ from __future__ import annotations
|
|
| 16 |
from voyager_verification import mechanical_verdict, verify_task
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class VerifiedCreativeCritic:
|
| 20 |
def __init__(self, upstream_critic):
|
| 21 |
self.upstream = upstream_critic
|
|
@@ -32,7 +45,7 @@ class VerifiedCreativeCritic:
|
|
| 32 |
model_success, model_critique = self.upstream.check_task_success(
|
| 33 |
events=events,
|
| 34 |
task=task,
|
| 35 |
-
context=f"{context}\nIndependent world observation: {evidence}",
|
| 36 |
chest_observation=chest_observation,
|
| 37 |
max_retries=max_retries,
|
| 38 |
)
|
|
|
|
| 16 |
from voyager_verification import mechanical_verdict, verify_task
|
| 17 |
|
| 18 |
|
| 19 |
+
# Facts the upstream critic prompt cannot know about this deployment. Appended
|
| 20 |
+
# to the context (never to critic.txt, never to an official method): Creative
|
| 21 |
+
# mining breaks blocks without drops, so only the verified-blocks line proves
|
| 22 |
+
# a mine; 'wood log' is any 1.19 log; obtaining is not mining.
|
| 23 |
+
CREATIVE_EVIDENCE_FACTS = (
|
| 24 |
+
"Creative mode facts: mining changes blocks but produces no item drops, so "
|
| 25 |
+
"a Mine task is proven only by the verified-blocks line above, never by "
|
| 26 |
+
"inventory deltas, chat messages or /give commands. 'wood log' means any "
|
| 27 |
+
"1.19 log (oak, spruce, birch, jungle, acacia, dark_oak, mangrove). "
|
| 28 |
+
"Obtaining an item and mining a block are different objectives."
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
class VerifiedCreativeCritic:
|
| 33 |
def __init__(self, upstream_critic):
|
| 34 |
self.upstream = upstream_critic
|
|
|
|
| 45 |
model_success, model_critique = self.upstream.check_task_success(
|
| 46 |
events=events,
|
| 47 |
task=task,
|
| 48 |
+
context=f"{context}\nIndependent world observation: {evidence}\n{CREATIVE_EVIDENCE_FACTS}",
|
| 49 |
chest_observation=chest_observation,
|
| 50 |
max_retries=max_retries,
|
| 51 |
)
|
voyager_verification.py
CHANGED
|
@@ -237,6 +237,24 @@ def task_history(tasks, limit=48):
|
|
| 237 |
return ", ".join(entries) or "None"
|
| 238 |
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
# The objective kinds verify_task() actually has a mechanical postcondition for.
|
| 241 |
# Anything else (kill, use, breed, ...) returns None from mechanical_verdict, so
|
| 242 |
# the Critic keeps full authority over it.
|
|
@@ -289,20 +307,29 @@ def verify_task(events, task):
|
|
| 289 |
return False, "Objective is not a simple quantified verb form; judge from the observation."
|
| 290 |
kind, amount, name = spec
|
| 291 |
if kind in {"mine", "place"}:
|
|
|
|
|
|
|
|
|
|
| 292 |
positions = set()
|
|
|
|
| 293 |
for evidence in final.get("verified_actions", []):
|
| 294 |
-
if evidence.get("kind") != kind or evidence.get("name")
|
| 295 |
continue
|
|
|
|
| 296 |
for position in evidence.get("positions", []):
|
| 297 |
if isinstance(position, list) and len(position) == 3 and all(
|
| 298 |
isinstance(v, (int, float)) and math.isfinite(v) for v in position
|
| 299 |
):
|
| 300 |
positions.add(tuple(position))
|
|
|
|
|
|
|
|
|
|
| 301 |
return len(positions) >= amount, (
|
| 302 |
-
f"Verified {len(positions)}/{amount} distinct {
|
| 303 |
)
|
| 304 |
if kind in {"obtain", "craft", "smelt", "cook"}:
|
| 305 |
-
|
|
|
|
| 306 |
return isinstance(count, (int, float)) and count >= amount, (
|
| 307 |
f"Live Inventory has {count}/{amount} {name}."
|
| 308 |
)
|
|
|
|
| 237 |
return ", ".join(entries) or "None"
|
| 238 |
|
| 239 |
|
| 240 |
+
# "Mine 1 wood log" names no species. In 1.19 a log is one of these overworld
|
| 241 |
+
# logs (stems are not logs). A generic name must match any of them, in mined
|
| 242 |
+
# blocks and in inventory alike; otherwise a real oak_log mine reads as
|
| 243 |
+
# "Verified 0/1 distinct wood_log" and both the model and the veto fail it.
|
| 244 |
+
WOOD_LOG_IDS = frozenset({
|
| 245 |
+
"oak_log", "spruce_log", "birch_log", "jungle_log",
|
| 246 |
+
"acacia_log", "dark_oak_log", "mangrove_log",
|
| 247 |
+
})
|
| 248 |
+
GENERIC_WOOD_NAMES = frozenset({"wood_log", "log", "logs", "wood", "woods"})
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _resolve_names(name):
|
| 252 |
+
"""Expand a generic item name to the concrete 1.19 IDs it can mean."""
|
| 253 |
+
if name in GENERIC_WOOD_NAMES:
|
| 254 |
+
return sorted(WOOD_LOG_IDS)
|
| 255 |
+
return [name]
|
| 256 |
+
|
| 257 |
+
|
| 258 |
# The objective kinds verify_task() actually has a mechanical postcondition for.
|
| 259 |
# Anything else (kill, use, breed, ...) returns None from mechanical_verdict, so
|
| 260 |
# the Critic keeps full authority over it.
|
|
|
|
| 307 |
return False, "Objective is not a simple quantified verb form; judge from the observation."
|
| 308 |
kind, amount, name = spec
|
| 309 |
if kind in {"mine", "place"}:
|
| 310 |
+
# Mining is proven by changed blocks, never by inventory: Creative
|
| 311 |
+
# produces no drops, and a /give or chat message is not a mine.
|
| 312 |
+
wanted = _resolve_names(name)
|
| 313 |
positions = set()
|
| 314 |
+
matched = set()
|
| 315 |
for evidence in final.get("verified_actions", []):
|
| 316 |
+
if evidence.get("kind") != kind or evidence.get("name") not in wanted:
|
| 317 |
continue
|
| 318 |
+
matched.add(evidence.get("name"))
|
| 319 |
for position in evidence.get("positions", []):
|
| 320 |
if isinstance(position, list) and len(position) == 3 and all(
|
| 321 |
isinstance(v, (int, float)) and math.isfinite(v) for v in position
|
| 322 |
):
|
| 323 |
positions.add(tuple(position))
|
| 324 |
+
detail = name if len(wanted) == 1 else (
|
| 325 |
+
f"{name} (as {', '.join(sorted(matched)) if matched else 'no matching log mined'})"
|
| 326 |
+
)
|
| 327 |
return len(positions) >= amount, (
|
| 328 |
+
f"Verified {len(positions)}/{amount} distinct {detail} blocks for {kind}."
|
| 329 |
)
|
| 330 |
if kind in {"obtain", "craft", "smelt", "cook"}:
|
| 331 |
+
inventory = final.get("inventory") or {}
|
| 332 |
+
count = sum(inventory.get(item, 0) for item in _resolve_names(name))
|
| 333 |
return isinstance(count, (int, float)) and count >= amount, (
|
| 334 |
f"Live Inventory has {count}/{amount} {name}."
|
| 335 |
)
|