Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ To install our models, install ollama and run the following terminal command:
ollama pull sweaterdog/andy-4:micro-q8_0 && ollama pull embeddinggemma
```

## Structured Action Logging

Set `"action_logging": true` in `settings.js` to write optional structured `ACTION` logs for successful wrapped skill calls. These logs can help debug and analyze block and item interactions.

Logs are written to:

```text
./bots/<bot-name>/logs/log_<timestamp>.txt
```

Example log line:

```text
ACTION function=placeBlock args=["dirt",4,-61,1] status=success item=dirt result_block=dirt result_coord=[4,-61,1] clicked_block=unknown clicked_face=unknown player_pos=[3.472681,-60.000000,2.516543] yaw=-493.917 pitch=58.176 sneaking=false standing_on=[3,-61,2] standing_on_block=grass_block hand=main_hand tick=1011330 timestamp=2026-05-02T16:56:42.367Z
```

Logged entries include the wrapped skill function name, serializable arguments, and common bot context where available. Exact action-specific fields such as clicked block and clicked face may be `unknown` when the wrapper cannot obtain them generically. Currently wrapped skill calls include block breaking, ordinary block placement, fluid placement, bucket use, and generic item use on blocks. Actions that bypass the instrumented shared skill functions are not automatically logged.

## Online Servers
To connect to online servers your bot will need an official Microsoft/Minecraft account. You can use your own personal one, but will need another account if you want to connect too and play with it. To connect, change these lines in `settings.js`:
```javascript
Expand Down
1 change: 1 addition & 0 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const settings = {

"spawn_timeout": 30, // num seconds allowed for the bot to spawn before throwing error. Increase when spawning takes a while.
"block_place_delay": 0, // delay between placing blocks (ms) if using newAction. helps avoid bot being kicked by anti-cheat mechanisms on servers.
"action_logging": false, // when true, write structured ACTION logs for successful bot block/item actions.

"log_all_prompts": false, // log ALL prompts to file
};
Expand Down
260 changes: 260 additions & 0 deletions src/agent/action_logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { appendFileSync, mkdirSync, writeFileSync } from 'fs';
import path from 'path';
import settings from '../../settings.js';

const ACTION_LOG_STATE = Symbol.for('mindcraft.actionLogState');
const ACTION_LOG_DEPTH = Symbol.for('mindcraft.actionLogDepth');

export function isActionLoggingEnabled() {
return Boolean(settings.action_logging ?? settings.action_logging_enabled ?? false);
}

function timestampForFilename(date = new Date()) {
return date.toISOString().replace(/[:.]/g, '-');
}

function safeName(name, fallback = 'andy') {
return String(name || fallback).replace(/[^a-zA-Z0-9_-]/g, '_');
}

function getBotLogDirectory(bot) {
const botName = safeName(bot?.username || bot?.name);
return path.join('.', 'bots', botName, 'logs');
}

function getActionLogPath(bot) {
if (!bot[ACTION_LOG_STATE]) {
const logDir = getBotLogDirectory(bot);
mkdirSync(logDir, { recursive: true });
const logPath = path.resolve(logDir, `log_${timestampForFilename()}.txt`);
writeFileSync(logPath, '', 'utf8');
bot[ACTION_LOG_STATE] = { logPath };
}
return bot[ACTION_LOG_STATE].logPath;
}

function blockCoord(coord) {
if (!coord) return null;
return {
x: Math.floor(coord.x),
y: Math.floor(coord.y),
z: Math.floor(coord.z),
};
}

function entityPosition(bot) {
const pos = bot?.entity?.position;
if (!pos) return null;
return { x: pos.x, y: pos.y, z: pos.z };
}

function standingState(bot) {
const pos = bot?.entity?.position;
if (!pos) {
return { position: null, blockName: 'unknown' };
}
const standing = {
x: Math.floor(pos.x),
y: Math.floor(pos.y - 0.01),
z: Math.floor(pos.z),
};
const blockName = blockNameAt(bot, standing);
return { position: standing, blockName };
}

function blockNameAt(bot, coord) {
if (!bot || !coord) return 'unknown';
try {
const pos = {
x: Math.floor(coord.x),
y: Math.floor(coord.y),
z: Math.floor(coord.z),
};
pos.floor = () => pos;
pos.floored = () => pos;
const block = bot.blockAt(pos);
return block?.name || 'unknown';
} catch (_) {
return 'unknown';
}
}

function heldItemName(bot) {
return bot?.heldItem?.name || 'none';
}

function getSneakState(bot) {
if (typeof bot?.getControlState === 'function') {
return Boolean(bot.getControlState('sneak'));
}
return Boolean(bot?.controlState?.sneak);
}

function getBotTick(bot) {
const candidates = [bot?.time?.age, bot?.time?.time, bot?.time?.timeOfDay];
return candidates.find(value => Number.isFinite(value)) ?? 'unknown';
}

function radToDeg(radians) {
return radians * 180 / Math.PI;
}

function actualYawDegrees(bot) {
return Number.isFinite(bot?.entity?.yaw) ? radToDeg(bot.entity.yaw) : null;
}

function actualPitchDegrees(bot) {
return Number.isFinite(bot?.entity?.pitch) ? radToDeg(bot.entity.pitch) : null;
}

function actionValue(value) {
if (value === null || value === undefined || value === '') return 'unknown';
return String(value).replace(/\s+/g, '_');
}

function intCoordText(coord) {
if (!coord) return 'unknown';
return `[${Math.floor(coord.x)},${Math.floor(coord.y)},${Math.floor(coord.z)}]`;
}

function floatText(value, precision = 6) {
return Number.isFinite(value) ? Number(value).toFixed(precision) : 'unknown';
}

function floatCoordText(coord, precision = 6) {
if (!coord) return 'unknown';
return `[${floatText(coord.x, precision)},${floatText(coord.y, precision)},${floatText(coord.z, precision)}]`;
}

function actionNumber(value, precision = 3) {
return Number.isFinite(value) ? Number(value).toFixed(precision) : 'unknown';
}

function coordArray(coord) {
if (!coord || !Number.isFinite(coord.x) || !Number.isFinite(coord.y) || !Number.isFinite(coord.z)) {
return null;
}
return [coord.x, coord.y, coord.z];
}

function serializeArg(value, seen = new Set(), depth = 0) {
if (value === undefined) return 'undefined';
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (depth > 2) return '[Object]';
if (Array.isArray(value)) {
return value.map(item => serializeArg(item, seen, depth + 1));
}
if (typeof value === 'object') {
if (seen.has(value)) return '[Circular]';
if (value.name && value.position) {
return {
name: value.name,
position: coordArray(value.position),
};
}
if (Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.z)) {
return coordArray(value);
}

seen.add(value);
const result = {};
for (const key of Object.keys(value).sort()) {
const item = value[key];
if (typeof item === 'function') continue;
result[key] = serializeArg(item, seen, depth + 1);
}
seen.delete(value);
return result;
}
return String(value);
}

function serializeArgs(args) {
return JSON.stringify(args.slice(1).map(value => serializeArg(value)));
}

export function formatActionLogLine(record) {
return [
'ACTION',
`function=${actionValue(record.functionName)}`,
`args=${actionValue(record.args)}`,
`status=${actionValue(record.status)}`,
`item=${actionValue(record.item)}`,
`result_block=${actionValue(record.resultBlock)}`,
`result_coord=${intCoordText(record.resultCoord)}`,
`clicked_block=${intCoordText(record.clickedBlock)}`,
`clicked_face=${actionValue(record.clickedFace)}`,
`player_pos=${floatCoordText(record.playerPos, 6)}`,
`yaw=${actionNumber(record.yaw, 3)}`,
`pitch=${actionNumber(record.pitch, 3)}`,
`sneaking=${record.sneaking === null || record.sneaking === undefined ? 'unknown' : String(Boolean(record.sneaking))}`,
`standing_on=${intCoordText(record.standingOn)}`,
`standing_on_block=${actionValue(record.standingOnBlock)}`,
`hand=${actionValue(record.hand)}`,
`tick=${actionValue(record.tick)}`,
`timestamp=${actionValue(record.timestamp)}`,
].join(' ');
}

function appendActionInvocationLog(bot, functionName, args, result, metadataAdapter) {
try {
const metadata = metadataAdapter?.(args, result) || {};
const standing = standingState(bot);
const resultCoord = blockCoord(metadata.resultCoord);
const record = {
functionName,
args: serializeArgs(args),
status: 'success',
item: metadata.item || heldItemName(bot),
resultBlock: metadata.resultBlock ?? blockNameAt(bot, resultCoord),
resultCoord,
clickedBlock: blockCoord(metadata.clickedBlock),
clickedFace: metadata.clickedFace,
playerPos: entityPosition(bot),
yaw: actualYawDegrees(bot),
pitch: actualPitchDegrees(bot),
sneaking: getSneakState(bot),
standingOn: standing.position,
standingOnBlock: standing.blockName,
hand: metadata.hand || 'main_hand',
tick: getBotTick(bot),
timestamp: new Date().toISOString(),
};
appendFileSync(getActionLogPath(bot), `${formatActionLogLine(record)}\n`, 'utf8');
return true;
} catch (error) {
console.warn(`Failed to append action log: ${error.message}`);
return false;
}
}

export function withActionLogging(functionName, actionFunction, metadataAdapter = null) {
const wrappedActionFunction = async function(...args) {
const bot = args[0];
if (!bot || !isActionLoggingEnabled()) {
return await actionFunction(...args);
}

const depth = bot[ACTION_LOG_DEPTH] || 0;
bot[ACTION_LOG_DEPTH] = depth + 1;

let result;
try {
result = await actionFunction(...args);
} finally {
bot[ACTION_LOG_DEPTH] = depth;
}

if (depth === 0 && result !== false) {
appendActionInvocationLog(bot, functionName, args, result, metadataAdapter);
}

return result;
};

Object.defineProperty(wrappedActionFunction, 'name', { value: functionName, configurable: true });
wrappedActionFunction.toString = () => actionFunction.toString();
return wrappedActionFunction;
}
31 changes: 28 additions & 3 deletions src/agent/library/skills.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as world from "./world.js";
import pf from 'mineflayer-pathfinder';
import Vec3 from 'vec3';
import settings from "../../../settings.js";
import { withActionLogging } from '../action_logger.js';

const blockPlaceDelay = settings.block_place_delay == null ? 0 : settings.block_place_delay;
const useDelay = blockPlaceDelay > 0;
Expand All @@ -11,6 +12,10 @@ export function log(bot, message) {
bot.output += message + '\n';
}

function logCoordFromXYZ(x, y, z) {
return { x, y, z };
}

async function autoLight(bot) {
if (world.shouldPlaceTorch(bot)) {
try {
Expand Down Expand Up @@ -558,7 +563,7 @@ export async function pickupNearbyItems(bot) {
}


export async function breakBlockAt(bot, x, y, z) {
async function breakBlockAtImpl(bot, x, y, z) {
/**
* Break the block at the given position. Will use the bot's equipped item.
* @param {MinecraftBot} bot, reference to the minecraft bot.
Expand Down Expand Up @@ -607,8 +612,17 @@ export async function breakBlockAt(bot, x, y, z) {
return true;
}

export const breakBlockAt = withActionLogging('breakBlockAt', breakBlockAtImpl, ([, x, y, z]) => {
const coord = logCoordFromXYZ(x, y, z);
return {
resultCoord: coord,
clickedBlock: coord,
clickedFace: 'unknown',
};
});


export async function placeBlock(bot, blockType, x, y, z, placeOn='bottom', dontCheat=false) {
async function placeBlockImpl(bot, blockType, x, y, z, placeOn='bottom', dontCheat=false) {
/**
* Place the given block type at the given position. It will build off from any adjacent blocks. Will fail if there is a block in the way or nothing to build off of.
* @param {MinecraftBot} bot, reference to the minecraft bot.
Expand Down Expand Up @@ -788,6 +802,11 @@ export async function placeBlock(bot, blockType, x, y, z, placeOn='bottom', dont
}
}

export const placeBlock = withActionLogging('placeBlock', placeBlockImpl, ([, , x, y, z]) => ({
resultCoord: logCoordFromXYZ(x, y, z),
clickedFace: 'unknown',
}));

export async function equip(bot, itemName) {
/**
* Equip the given item to the proper body part, like tools or armor.
Expand Down Expand Up @@ -2041,7 +2060,7 @@ export async function useToolOn(bot, toolName, targetName) {
return true;
}

export async function useToolOnBlock(bot, toolName, block) {
async function useToolOnBlockImpl(bot, toolName, block) {
/**
* Use a tool on a specific block.
* @param {MinecraftBot} bot
Expand Down Expand Up @@ -2091,3 +2110,9 @@ export async function useToolOn(bot, toolName, targetName) {
log(bot, `Used ${toolName} on ${block.name}.`);
return true;
}

export const useToolOnBlock = withActionLogging('useToolOnBlock', useToolOnBlockImpl, ([, , block]) => ({
resultCoord: block?.position,
clickedBlock: block?.position,
clickedFace: 'unknown',
}));
7 changes: 6 additions & 1 deletion src/mindcraft/public/settings_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,10 @@
"type": "number",
"description": "Number of seconds allowed for the bot to spawn before throwing an error. Increase when spawning takes a while.",
"default": 30
},
"action_logging": {
"type": "boolean",
"description": "Whether to write structured ACTION logs for successful bot block and item actions.",
"default": false
}
}
}