Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
57 changes: 57 additions & 0 deletions ACTION_LOGGING.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not important enough to have its own file. Consider moving to README.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i removed the actionlogging md in the new commit and merged it more succintly in the readme

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Structured Action Logging

Mindcraft can optionally write structured `ACTION` lines for successful bot actions that change or use blocks. The log is intended to make Andy's physical actions inspectable without changing how the actions are executed.

## Enabling

Set `action_logging` to `true` in `settings.js`:

```js
"action_logging": true,
"action_log_file_prefix": "andy_log",
```

Logging is disabled by default so normal Mindcraft behavior stays unchanged unless the feature is explicitly enabled.

## Log Location

When the first action is logged, Mindcraft creates a new file for the current bot:

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

The filename prefix can be changed with `action_log_file_prefix`.

## Log Format

Each logged action is one line:

```text
ACTION <type> item=<item> previous_block=<block> result_block=<block> result_coord=[x,y,z] clicked_block=[x,y,z] clicked_face=<face> player_pos=[x.xxxxxx,y.yyyyyy,z.zzzzzz] yaw=<degrees> pitch=<degrees> sneaking=<true|false> standing_on=[x,y,z] standing_on_block=<block> hand=<main_hand|off_hand> tick=<tick> timestamp=<iso-date>
```

Example:

```text
ACTION place_block item=dirt previous_block=air result_block=dirt result_coord=[4,-61,1] clicked_block=[4,-62,1] clicked_face=up 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
```

Unknown or unavailable fields are written as `unknown`.

## Logged Actions

The logger currently records successful actions from the standard skill execution paths:

- `break_block` from `breakBlockAt`
- `place_block` from normal `placeBlock`
- `place_fluid` from bucket-based fluid placement through `placeBlock` or `useToolOnBlock`
- `use_bucket` from empty bucket use on water or lava through `useToolOnBlock`
- `use_item_on_block` from generic tool/item use through `useToolOnBlock`

## Known Limitations

- The logger records actions that go through the shared skill functions. Direct Mineflayer calls made elsewhere are not automatically logged.
- Some interactions do not expose an exact clicked face through the existing skill API; those lines use `clicked_face=unknown`.
- Cheat-mode `/setblock` placement and breaking are logged with the target coordinate, but they do not have a real clicked block face.
- Entity interactions are not logged.
2 changes: 2 additions & 0 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ 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.
"action_log_file_prefix": "andy_log", // prefix for action log files in ./bots/<bot>/logs/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be hard coded. Or just use log by default. Maybe also with a timestamp.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i made the log name log__.... instead of andy_log in the new commit.


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

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

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 prefix = safeName(settings.action_log_file_prefix || 'andy_log', 'andy_log');
const logPath = path.resolve(logDir, `${prefix}_${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);
}

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';
}

export function faceNameFromVector(vec) {
if (!vec) return 'unknown';
if (vec.x === 1 && vec.y === 0 && vec.z === 0) return 'east';
if (vec.x === -1 && vec.y === 0 && vec.z === 0) return 'west';
if (vec.x === 0 && vec.y === 1 && vec.z === 0) return 'up';
if (vec.x === 0 && vec.y === -1 && vec.z === 0) return 'down';
if (vec.x === 0 && vec.y === 0 && vec.z === 1) return 'south';
if (vec.x === 0 && vec.y === 0 && vec.z === -1) return 'north';
return 'unknown';
}

export function formatActionLogLine(record) {
return [
`ACTION ${record.type}`,
`item=${actionValue(record.item)}`,
`previous_block=${actionValue(record.previousBlock)}`,
`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(' ');
}

export function appendActionLog(bot, action) {
if (!bot || !isActionLoggingEnabled()) {
return false;
}

try {
const standing = standingState(bot);
const resultCoord = blockCoord(action.resultCoord);
const record = {
type: action.type,
item: action.item || heldItemName(bot),
previousBlock: action.previousBlock,
resultBlock: action.resultBlock ?? blockNameAt(bot, resultCoord),
resultCoord,
clickedBlock: blockCoord(action.clickedBlock),
clickedFace: action.clickedFace,
playerPos: entityPosition(bot),
yaw: actualYawDegrees(bot),
pitch: actualPitchDegrees(bot),
sneaking: getSneakState(bot),
standingOn: standing.position,
standingOnBlock: standing.blockName,
hand: action.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;
}
}
Loading