naldo commited on
Commit
0bccbe6
·
1 Parent(s): 7e83939

Skip Creative chest grind and report Gemini token usage

Browse files
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.models && result.stats.models[MODEL] && result.stats.models[MODEL].tokens;
76
- state.prompt_tokens = usage && (usage.prompt || usage.input) || null;
77
- state.completion_tokens = usage && (usage.candidates || usage.output) || null;
 
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)',