naldo commited on
Commit ·
0bccbe6
1
Parent(s): 7e83939
Skip Creative chest grind and report Gemini token usage
Browse files- gemini_bot.js +6 -5
- gemini_bot_logic.js +13 -1
- tests/test_gemini_bot.js +8 -1
- tests/test_voyager_patch.py +9 -1
- voyager_stack.py +12 -2
gemini_bot.js
CHANGED
|
@@ -8,7 +8,7 @@ const mineflayer = require("mineflayer");
|
|
| 8 |
const { pathfinder, Movements, goals: { GoalNear } } = require("mineflayer-pathfinder");
|
| 9 |
const mcData = require("minecraft-data");
|
| 10 |
const Vec3 = require("vec3");
|
| 11 |
-
const { DIRECTIONS, parseDecision, extractCliResponse, makePrompt } = require("./gemini_bot_logic");
|
| 12 |
|
| 13 |
const USERNAME = "Gemini_Bot";
|
| 14 |
const ADMIN = "IffyBinkie6134";
|
|
@@ -19,7 +19,7 @@ const WORKSPACE = process.env.GEMINI_BOT_WORKSPACE;
|
|
| 19 |
const PORT = Number(process.env.GEMINI_BOT_STATUS_PORT || "3001");
|
| 20 |
const state = { ready: false, busy: false, reconnect_count: 0, completed_actions: 0,
|
| 21 |
failed_actions: 0, cli_calls: 0, last_action: null, last_error: null, last_elapsed_s: null,
|
| 22 |
-
prompt_tokens: null, completion_tokens: null, model: MODEL, bot_username: USERNAME };
|
| 23 |
let bot = null;
|
| 24 |
let reconnectTimer = null;
|
| 25 |
let busy = false;
|
|
@@ -72,9 +72,10 @@ function callCli(prompt) {
|
|
| 72 |
}
|
| 73 |
try {
|
| 74 |
const result = extractCliResponse(stdout);
|
| 75 |
-
const usage = result.stats
|
| 76 |
-
state.prompt_tokens = usage
|
| 77 |
-
state.completion_tokens = usage
|
|
|
|
| 78 |
resolve(result.decision);
|
| 79 |
} catch (_) { reject(new Error("Gemini CLI response malformed")); }
|
| 80 |
});
|
|
|
|
| 8 |
const { pathfinder, Movements, goals: { GoalNear } } = require("mineflayer-pathfinder");
|
| 9 |
const mcData = require("minecraft-data");
|
| 10 |
const Vec3 = require("vec3");
|
| 11 |
+
const { DIRECTIONS, parseDecision, extractCliResponse, tokenUsage, makePrompt } = require("./gemini_bot_logic");
|
| 12 |
|
| 13 |
const USERNAME = "Gemini_Bot";
|
| 14 |
const ADMIN = "IffyBinkie6134";
|
|
|
|
| 19 |
const PORT = Number(process.env.GEMINI_BOT_STATUS_PORT || "3001");
|
| 20 |
const state = { ready: false, busy: false, reconnect_count: 0, completed_actions: 0,
|
| 21 |
failed_actions: 0, cli_calls: 0, last_action: null, last_error: null, last_elapsed_s: null,
|
| 22 |
+
prompt_tokens: null, completion_tokens: null, models_used: [], model: MODEL, bot_username: USERNAME };
|
| 23 |
let bot = null;
|
| 24 |
let reconnectTimer = null;
|
| 25 |
let busy = false;
|
|
|
|
| 72 |
}
|
| 73 |
try {
|
| 74 |
const result = extractCliResponse(stdout);
|
| 75 |
+
const usage = tokenUsage(result.stats);
|
| 76 |
+
state.prompt_tokens = usage.prompt_tokens;
|
| 77 |
+
state.completion_tokens = usage.completion_tokens;
|
| 78 |
+
state.models_used = usage.models_used;
|
| 79 |
resolve(result.decision);
|
| 80 |
} catch (_) { reject(new Error("Gemini CLI response malformed")); }
|
| 81 |
});
|
gemini_bot_logic.js
CHANGED
|
@@ -46,6 +46,18 @@ function extractCliResponse(output) {
|
|
| 46 |
return { decision: parseDecision(envelope.response), stats: envelope.stats || {} };
|
| 47 |
}
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
function makePrompt(observation, request) {
|
| 50 |
const instruction = request ? `Admin request: ${JSON.stringify(request.slice(0, 240))}` :
|
| 51 |
"Autonomous turn: choose explore, mine, or place to make one small real change. Use wait only if blocked; save chat for admin requests.";
|
|
@@ -63,4 +75,4 @@ function makePrompt(observation, request) {
|
|
| 63 |
].join("\n");
|
| 64 |
}
|
| 65 |
|
| 66 |
-
module.exports = { DIRECTIONS, PLACE_PALETTE, MINE_PALETTE, parseDecision, extractCliResponse, makePrompt };
|
|
|
|
| 46 |
return { decision: parseDecision(envelope.response), stats: envelope.stats || {} };
|
| 47 |
}
|
| 48 |
|
| 49 |
+
function tokenUsage(stats) {
|
| 50 |
+
const models = stats && stats.models && typeof stats.models === "object" ? stats.models : {};
|
| 51 |
+
const tokenSets = Object.values(models).map(value => value && value.tokens || {});
|
| 52 |
+
if (!tokenSets.length && stats && stats.tokens) tokenSets.push(stats.tokens);
|
| 53 |
+
const sum = names => tokenSets.reduce((total, tokens) => {
|
| 54 |
+
const key = names.find(name => Number.isFinite(tokens[name]));
|
| 55 |
+
return total + (key ? tokens[key] : 0);
|
| 56 |
+
}, 0);
|
| 57 |
+
return { models_used: Object.keys(models), prompt_tokens: sum(["prompt", "input", "inputTokens"]),
|
| 58 |
+
completion_tokens: sum(["candidates", "output", "outputTokens"]) };
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
function makePrompt(observation, request) {
|
| 62 |
const instruction = request ? `Admin request: ${JSON.stringify(request.slice(0, 240))}` :
|
| 63 |
"Autonomous turn: choose explore, mine, or place to make one small real change. Use wait only if blocked; save chat for admin requests.";
|
|
|
|
| 75 |
].join("\n");
|
| 76 |
}
|
| 77 |
|
| 78 |
+
module.exports = { DIRECTIONS, PLACE_PALETTE, MINE_PALETTE, parseDecision, extractCliResponse, tokenUsage, makePrompt };
|
tests/test_gemini_bot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"use strict";
|
| 2 |
const assert = require("node:assert/strict");
|
| 3 |
const { test } = require("node:test");
|
| 4 |
-
const { parseDecision, extractCliResponse, makePrompt } = require("../gemini_bot_logic");
|
| 5 |
|
| 6 |
test("accepts one bounded action", () => {
|
| 7 |
assert.deepEqual(parseDecision('{"action":"explore","direction":"north","blocks":16}'),
|
|
@@ -33,6 +33,13 @@ test("reads CLI envelope, and fails closed on API errors", () => {
|
|
| 33 |
assert.throws(() => extractCliResponse("invalid"));
|
| 34 |
});
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
test("prompt contains one observed world and the short admin request", () => {
|
| 37 |
const prompt = makePrompt({ position: { x: 0, y: 70, z: 0 }, nearby_blocks: ["stone"] }, "segue o Naldo");
|
| 38 |
assert.match(prompt, /Naldo_Bot/);
|
|
|
|
| 1 |
"use strict";
|
| 2 |
const assert = require("node:assert/strict");
|
| 3 |
const { test } = require("node:test");
|
| 4 |
+
const { parseDecision, extractCliResponse, tokenUsage, makePrompt } = require("../gemini_bot_logic");
|
| 5 |
|
| 6 |
test("accepts one bounded action", () => {
|
| 7 |
assert.deepEqual(parseDecision('{"action":"explore","direction":"north","blocks":16}'),
|
|
|
|
| 33 |
assert.throws(() => extractCliResponse("invalid"));
|
| 34 |
});
|
| 35 |
|
| 36 |
+
test("aggregates per-model CLI token metrics", () => {
|
| 37 |
+
assert.deepEqual(tokenUsage({ models: { "gemini-3.1-flash-lite": { tokens: { prompt: 120, candidates: 25 } } } }),
|
| 38 |
+
{ models_used: ["gemini-3.1-flash-lite"], prompt_tokens: 120, completion_tokens: 25 });
|
| 39 |
+
assert.deepEqual(tokenUsage({ tokens: { input: 10, output: 2 } }),
|
| 40 |
+
{ models_used: [], prompt_tokens: 10, completion_tokens: 2 });
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
test("prompt contains one observed world and the short admin request", () => {
|
| 44 |
const prompt = makePrompt({ position: { x: 0, y: 70, z: 0 }, nearby_blocks: ["stone"] }, "segue o Naldo");
|
| 45 |
assert.match(prompt, /Naldo_Bot/);
|
tests/test_voyager_patch.py
CHANGED
|
@@ -125,7 +125,7 @@ def test_resume():
|
|
| 125 |
def check_curriculum_behavior(source):
|
| 126 |
tree = ast.parse(source)
|
| 127 |
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "CurriculumAgent")
|
| 128 |
-
names = {"get_task_context", "propose_next_ai_task", "parse_ai_message", "render_human_message", "render_observation"}
|
| 129 |
methods = [n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name in names]
|
| 130 |
ns = {"AIMessage": SimpleNamespace, "HumanMessage": SimpleNamespace,
|
| 131 |
"choose_curriculum_task": choose_curriculum_task,
|
|
@@ -180,6 +180,13 @@ def check_curriculum_behavior(source):
|
|
| 180 |
task, _ = agent.propose_next_ai_task(messages=[SimpleNamespace(content="Nearby blocks: dirt, stone")])
|
| 181 |
assert task == "Mine 1 dirt" and agent.llm.call_count == 3
|
| 182 |
assert task_spec(task) not in {task_spec(old) for old in agent.completed_tasks + agent.failed_tasks}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
print("PASS valid, composite, duplicate, Survival, unlabeled and exhausted Curriculum outputs")
|
| 184 |
|
| 185 |
|
|
@@ -304,6 +311,7 @@ def check_clean_upstream(repo):
|
|
| 304 |
curriculum = (stack.SRC / "voyager/agents/curriculum.py").read_text()
|
| 305 |
assert "Creative deployment: skip Survival QA" in curriculum
|
| 306 |
assert "Curriculum fallback after bounded retries" in curriculum
|
|
|
|
| 307 |
check_curriculum_behavior(curriculum)
|
| 308 |
check_action_behavior((stack.SRC / "voyager/agents/action.py").read_text())
|
| 309 |
exploration = (stack.SRC / "voyager/control_primitives/exploreUntil.js").read_text()
|
|
|
|
| 125 |
def check_curriculum_behavior(source):
|
| 126 |
tree = ast.parse(source)
|
| 127 |
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "CurriculumAgent")
|
| 128 |
+
names = {"get_task_context", "propose_next_ai_task", "propose_next_task", "parse_ai_message", "render_human_message", "render_observation"}
|
| 129 |
methods = [n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name in names]
|
| 130 |
ns = {"AIMessage": SimpleNamespace, "HumanMessage": SimpleNamespace,
|
| 131 |
"choose_curriculum_task": choose_curriculum_task,
|
|
|
|
| 180 |
task, _ = agent.propose_next_ai_task(messages=[SimpleNamespace(content="Nearby blocks: dirt, stone")])
|
| 181 |
assert task == "Mine 1 dirt" and agent.llm.call_count == 3
|
| 182 |
assert task_spec(task) not in {task_spec(old) for old in agent.completed_tasks + agent.failed_tasks}
|
| 183 |
+
agent.mode = "auto"
|
| 184 |
+
agent.render_system_message = Mock(return_value=SimpleNamespace(content="Creative"))
|
| 185 |
+
agent.render_human_message = Mock(return_value=SimpleNamespace(content="inventory full"))
|
| 186 |
+
agent.propose_next_ai_task = Mock(return_value=("Place 1 stone", "Creative action"))
|
| 187 |
+
full = {"status": {"inventoryUsed": 35}, "inventory": {}}
|
| 188 |
+
assert agent.propose_next_task(events=[["observe", full]], chest_observation="Chests: None\n\n") == ("Place 1 stone", "Creative action")
|
| 189 |
+
agent.propose_next_ai_task.assert_called_once()
|
| 190 |
print("PASS valid, composite, duplicate, Survival, unlabeled and exhausted Curriculum outputs")
|
| 191 |
|
| 192 |
|
|
|
|
| 311 |
curriculum = (stack.SRC / "voyager/agents/curriculum.py").read_text()
|
| 312 |
assert "Creative deployment: skip Survival QA" in curriculum
|
| 313 |
assert "Curriculum fallback after bounded retries" in curriculum
|
| 314 |
+
assert "Craft 1 chest" not in curriculum
|
| 315 |
check_curriculum_behavior(curriculum)
|
| 316 |
check_action_behavior((stack.SRC / "voyager/agents/action.py").read_text())
|
| 317 |
exploration = (stack.SRC / "voyager/control_primitives/exploreUntil.js").read_text()
|
voyager_stack.py
CHANGED
|
@@ -701,8 +701,18 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 701 |
r'(?ms)^ if self.progress >= self.warm_up\["context"\]:\n.*?(?=^ for key in self.curriculum_observations:)',
|
| 702 |
'', curriculum_text, count=1,
|
| 703 |
)
|
| 704 |
-
if qa_count != 1:
|
| 705 |
-
raise RuntimeError("Voyager Creative observation QA patch did not apply")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
curriculum_text = curriculum_text.replace('from __future__ import annotations', 'from __future__ import annotations\nfrom voyager_verification import validate_task, task_history', 1)
|
| 707 |
curriculum_text = curriculum_text.replace(
|
| 708 |
'", ".join(self.completed_tasks)', 'task_history(self.completed_tasks)',
|
|
|
|
| 701 |
r'(?ms)^ if self.progress >= self.warm_up\["context"\]:\n.*?(?=^ for key in self.curriculum_observations:)',
|
| 702 |
'', curriculum_text, count=1,
|
| 703 |
)
|
| 704 |
+
if qa_count != 1:
|
| 705 |
+
raise RuntimeError("Voyager Creative observation QA patch did not apply")
|
| 706 |
+
# Upstream's near-full inventory shortcut forces an endless Survival
|
| 707 |
+
# "Craft 1 chest" task at 33 slots. Creative has no such prerequisite:
|
| 708 |
+
# the bounded curriculum must choose a world action from observation.
|
| 709 |
+
curriculum_text, chest_count = re.subn(
|
| 710 |
+
r'(?ms)^ # hard code task when inventory is almost full\n.*?(?=^ messages = \[)',
|
| 711 |
+
' # Creative deployment skips the Survival chest shortcut.\n\n',
|
| 712 |
+
curriculum_text, count=1,
|
| 713 |
+
)
|
| 714 |
+
if chest_count != 1:
|
| 715 |
+
raise RuntimeError("Voyager Survival chest shortcut marker changed")
|
| 716 |
curriculum_text = curriculum_text.replace('from __future__ import annotations', 'from __future__ import annotations\nfrom voyager_verification import validate_task, task_history', 1)
|
| 717 |
curriculum_text = curriculum_text.replace(
|
| 718 |
'", ".join(self.completed_tasks)', 'task_history(self.completed_tasks)',
|