refactor(minecraft): better blackboard and task scheduling
This commit is contained in:
@@ -27,6 +27,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'inventory',
|
||||
description: 'Get your inventory.',
|
||||
execution: 'parallel',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => (): string => {
|
||||
const inventory = world.getInventoryCounts(mineflayer)
|
||||
@@ -49,6 +50,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'nearbyBlocks',
|
||||
description: 'Get the blocks near you.',
|
||||
execution: 'parallel',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => (): string => {
|
||||
const blocks = world.getNearbyBlockTypes(mineflayer)
|
||||
@@ -59,6 +61,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'craftable',
|
||||
description: 'Get the craftable items with your inventory.',
|
||||
execution: 'parallel',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => (): string => {
|
||||
const craftable = world.getCraftableItems(mineflayer)
|
||||
@@ -68,6 +71,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'entities',
|
||||
description: 'Get the nearby players and entities.',
|
||||
execution: 'parallel',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => (): string => {
|
||||
const players = world.getNearbyPlayerNames(mineflayer)
|
||||
@@ -85,6 +89,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'stop',
|
||||
description: 'Force stop all actions', // TODO: include name of the current action in description?
|
||||
execution: 'sequential',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => async () => {
|
||||
mineflayer.interrupt('stop tool called')
|
||||
@@ -95,6 +100,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'goToPlayer',
|
||||
description: 'Go to the given player.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
player_name: z.string().describe('The name of the player to go to.'),
|
||||
closeness: z.number().describe('How close to get to the player in blocks.').min(0),
|
||||
@@ -108,6 +114,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'followPlayer',
|
||||
description: 'Endlessly follow the given player.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
player_name: z.string().describe('name of the player to follow.'),
|
||||
follow_dist: z.number().describe('The distance to follow from.').min(0),
|
||||
@@ -120,6 +127,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'goToCoordinates',
|
||||
description: 'Go to the given x, y, z location.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
x: z.number().describe('The x coordinate.'),
|
||||
y: z.number().describe('The y coordinate.').min(-64).max(320),
|
||||
@@ -134,6 +142,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'searchForBlock',
|
||||
description: 'Find and go to the nearest block of a given type in a given range.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The block type to go to.'),
|
||||
search_range: z.number().describe('The range to search for the block.').min(32).max(512),
|
||||
@@ -146,6 +155,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'searchForEntity',
|
||||
description: 'Find and go to the nearest entity of a given type in a given range.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The type of entity to go to.'),
|
||||
search_range: z.number().describe('The range to search for the entity.').min(32).max(512),
|
||||
@@ -169,6 +179,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'givePlayer',
|
||||
description: 'Give the specified item to the given player.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
player_name: z.string().describe('The name of the player to give the item to.'),
|
||||
item_name: z.string().describe('The name of the item to give.'),
|
||||
@@ -182,6 +193,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'consume',
|
||||
description: 'Eat/drink the given item.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to consume.'),
|
||||
}),
|
||||
@@ -193,6 +205,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'equip',
|
||||
description: 'Equip the given item.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to equip.'),
|
||||
}),
|
||||
@@ -204,6 +217,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'putInChest',
|
||||
description: 'Put the given item in the nearest chest.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to put in the chest.'),
|
||||
num: z.number().int().describe('The number of items to put in the chest.').min(1),
|
||||
@@ -216,6 +230,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'takeFromChest',
|
||||
description: 'Take the given items from the nearest chest.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to take.'),
|
||||
num: z.number().int().describe('The number of items to take.').min(1),
|
||||
@@ -237,6 +252,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'discard',
|
||||
description: 'Discard the given item from the inventory.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to discard.'),
|
||||
num: z.number().int().describe('The number of items to discard.').min(1),
|
||||
@@ -249,6 +265,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'collectBlocks',
|
||||
description: 'Collect the nearest blocks of a given type.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The block type to collect.'),
|
||||
num: z.number().int().describe('The number of blocks to collect.').min(1),
|
||||
@@ -264,6 +281,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'craftRecipe',
|
||||
description: 'Craft the given recipe a given number of times.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
recipe_name: z.string().describe('The name of the output item to craft.'),
|
||||
num: z.number().int().describe('The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.').min(1),
|
||||
@@ -276,6 +294,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'smeltItem',
|
||||
description: 'Smelt the given item the given number of times.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the input item to smelt.'),
|
||||
num: z.number().int().describe('The number of times to smelt the item.').min(1),
|
||||
@@ -288,6 +307,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'clearFurnace',
|
||||
description: 'Take all items out of the nearest furnace.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => async () => {
|
||||
await skills.clearNearestFurnace(mineflayer)
|
||||
@@ -297,6 +317,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'placeHere',
|
||||
description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The block type to place.'),
|
||||
}),
|
||||
@@ -309,6 +330,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'attack',
|
||||
description: 'Attack and kill the nearest entity of a given type.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The type of entity to attack.'),
|
||||
}),
|
||||
@@ -320,6 +342,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'attackPlayer',
|
||||
description: 'Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
player_name: z.string().describe('The name of the player to attack.'),
|
||||
}),
|
||||
@@ -335,6 +358,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'goToBed',
|
||||
description: 'Go to the nearest bed and sleep.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({}),
|
||||
perform: mineflayer => async () => {
|
||||
await skills.goToBed(mineflayer)
|
||||
@@ -344,6 +368,7 @@ export const actionsList: Action[] = [
|
||||
{
|
||||
name: 'activate',
|
||||
description: 'Activate the nearest object of a given type.',
|
||||
execution: 'sequential',
|
||||
schema: z.object({
|
||||
type: z.string().describe('The type of object to activate.'),
|
||||
}),
|
||||
|
||||
@@ -105,8 +105,7 @@ export class TaskExecutor extends EventEmitter {
|
||||
|
||||
this.logger.withField('count', actions.length).log('Executing actions')
|
||||
|
||||
// Execute each action independently and asynchronously
|
||||
actions.forEach(async (action) => {
|
||||
const runSingleAction = async (action: ActionInstruction): Promise<void> => {
|
||||
if (cancellationToken?.isCancelled) {
|
||||
this.logger.log('Action execution cancelled before start')
|
||||
return
|
||||
@@ -116,7 +115,18 @@ export class TaskExecutor extends EventEmitter {
|
||||
|
||||
try {
|
||||
let result: string | void
|
||||
if (action.type === 'physical') {
|
||||
if (action.type === 'sequential' || action.type === 'parallel') {
|
||||
if (action.type === 'parallel') {
|
||||
const available = this.actionAgent.getAvailableActions()
|
||||
const def = available.find(a => a.name === action.step.tool)
|
||||
if (!def || def.execution !== 'parallel') {
|
||||
throw new ActionError('UNKNOWN', `Tool '${action.step.tool}' is not allowed for parallel actions`, {
|
||||
tool: action.step.tool,
|
||||
requestedExecution: action.type,
|
||||
allowedExecution: def?.execution,
|
||||
})
|
||||
}
|
||||
}
|
||||
result = await this.actionAgent.performAction(action.step)
|
||||
}
|
||||
else if (action.type === 'chat') {
|
||||
@@ -127,15 +137,7 @@ export class TaskExecutor extends EventEmitter {
|
||||
throw new Error(`Unknown action type: ${(action as any).type}`)
|
||||
}
|
||||
|
||||
if (cancellationToken?.isCancelled) {
|
||||
// If cancelled during execution (and agent didn't throw), we might still consider it cancelled?
|
||||
// But usually agents throw if cancelled.
|
||||
// Just emit completed for now if it finished.
|
||||
}
|
||||
|
||||
if (action.require_feedback) {
|
||||
this.emit('action:completed', { action, result })
|
||||
}
|
||||
this.emit('action:completed', { action, result })
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.withError(error).error('Action execution failed')
|
||||
@@ -146,8 +148,37 @@ export class TaskExecutor extends EventEmitter {
|
||||
|
||||
// failed actions always emit feedback
|
||||
this.emit('action:failed', { action, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const sequentialActions = actions.filter(a => a.type === 'sequential')
|
||||
const parallelActions = actions.filter(a => a.type === 'parallel' || a.type === 'chat')
|
||||
|
||||
// Fire and forget: parallel-safe actions can run concurrently.
|
||||
parallelActions.forEach((action) => {
|
||||
void runSingleAction(action).catch(() => {
|
||||
// errors are emitted via events; nothing else to do here
|
||||
})
|
||||
})
|
||||
|
||||
// Sequential actions must be executed strictly in order.
|
||||
void (async () => {
|
||||
for (const action of sequentialActions) {
|
||||
if (cancellationToken?.isCancelled) {
|
||||
this.logger.log('Action execution cancelled before start')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await runSingleAction(action)
|
||||
}
|
||||
catch (error) {
|
||||
// Fail fast: stop executing remaining physical actions.
|
||||
return
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
public getAvailableActions() {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import type { PlanStep } from '../../../agents/planning/adapter'
|
||||
|
||||
export type ActionType = 'physical' | 'chat'
|
||||
export type ActionType = 'sequential' | 'parallel' | 'chat'
|
||||
|
||||
export interface BaseActionInstruction {
|
||||
type: ActionType
|
||||
id?: string
|
||||
description?: string
|
||||
require_feedback?: boolean
|
||||
}
|
||||
|
||||
export interface PhysicalActionInstruction extends BaseActionInstruction {
|
||||
type: 'physical'
|
||||
export interface SequentialActionInstruction extends BaseActionInstruction {
|
||||
type: 'sequential'
|
||||
step: PlanStep
|
||||
}
|
||||
|
||||
export interface ParallelActionInstruction extends BaseActionInstruction {
|
||||
type: 'parallel'
|
||||
step: PlanStep
|
||||
}
|
||||
|
||||
@@ -18,4 +24,4 @@ export interface ChatActionInstruction extends BaseActionInstruction {
|
||||
message: string
|
||||
}
|
||||
|
||||
export type ActionInstruction = PhysicalActionInstruction | ChatActionInstruction
|
||||
export type ActionInstruction = SequentialActionInstruction | ParallelActionInstruction | ChatActionInstruction
|
||||
|
||||
@@ -15,12 +15,16 @@ export interface BlackboardState {
|
||||
strategy: string
|
||||
contextView: contextViewState
|
||||
chatHistory: ChatMessage[]
|
||||
recentActionHistory: string[]
|
||||
pendingActions: string[]
|
||||
selfUsername: string
|
||||
}
|
||||
|
||||
export class Blackboard {
|
||||
private _state: BlackboardState
|
||||
private static readonly MAX_CHAT_HISTORY = 8
|
||||
private static readonly MAX_ACTION_HISTORY = 12
|
||||
private static readonly MAX_PENDING_ACTIONS = 12
|
||||
|
||||
constructor() {
|
||||
this._state = {
|
||||
@@ -32,6 +36,8 @@ export class Blackboard {
|
||||
environmentSummary: 'Unknown',
|
||||
},
|
||||
chatHistory: [],
|
||||
recentActionHistory: [],
|
||||
pendingActions: [],
|
||||
selfUsername: 'Bot',
|
||||
}
|
||||
}
|
||||
@@ -43,6 +49,8 @@ export class Blackboard {
|
||||
public get selfSummary(): string { return this._state.contextView.selfSummary }
|
||||
public get environmentSummary(): string { return this._state.contextView.environmentSummary }
|
||||
public get chatHistory(): ChatMessage[] { return this._state.chatHistory }
|
||||
public get recentActionHistory(): string[] { return this._state.recentActionHistory }
|
||||
public get pendingActions(): string[] { return this._state.pendingActions }
|
||||
public get selfUsername(): string { return this._state.selfUsername }
|
||||
|
||||
// Setters (Partial updates allowed)
|
||||
@@ -62,11 +70,24 @@ export class Blackboard {
|
||||
this._state = { ...this._state, chatHistory: newHistory }
|
||||
}
|
||||
|
||||
public addActionHistoryLine(line: string): void {
|
||||
const next = [...this._state.recentActionHistory, line]
|
||||
const trimmed = next.length > Blackboard.MAX_ACTION_HISTORY ? next.slice(-Blackboard.MAX_ACTION_HISTORY) : next
|
||||
this._state = { ...this._state, recentActionHistory: trimmed }
|
||||
}
|
||||
|
||||
public setPendingActions(lines: string[]): void {
|
||||
const trimmed = lines.length > Blackboard.MAX_PENDING_ACTIONS ? lines.slice(0, Blackboard.MAX_PENDING_ACTIONS) : lines
|
||||
this._state = { ...this._state, pendingActions: trimmed }
|
||||
}
|
||||
|
||||
public getSnapshot(): BlackboardState {
|
||||
return {
|
||||
...this._state,
|
||||
contextView: { ...this._state.contextView },
|
||||
chatHistory: [...this._state.chatHistory],
|
||||
recentActionHistory: [...this._state.recentActionHistory],
|
||||
pendingActions: [...this._state.pendingActions],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ export class Brain {
|
||||
private blackboard: Blackboard
|
||||
private debugService: DebugService
|
||||
|
||||
private nextActionId = 1
|
||||
private inFlightActions = new Map<string, ActionInstruction>()
|
||||
|
||||
private feedbackDebounceMs = Number.parseInt(process.env.BRAIN_FEEDBACK_DEBOUNCE_MS ?? '200')
|
||||
private feedbackDebounceTimer: NodeJS.Timeout | undefined
|
||||
|
||||
// Event Queue
|
||||
private queue: QueuedEvent[] = []
|
||||
private isProcessing = false
|
||||
@@ -97,8 +103,25 @@ export class Brain {
|
||||
})
|
||||
|
||||
// Listen to Task Execution Events (Action Feedback)
|
||||
this.deps.taskExecutor.on('action:started', ({ action }) => {
|
||||
const id = action.id
|
||||
if (id)
|
||||
this.inFlightActions.set(id, action)
|
||||
this.updatePendingActionsOnBlackboard()
|
||||
})
|
||||
|
||||
this.deps.taskExecutor.on('action:completed', async ({ action, result }) => {
|
||||
this.log('INFO', `Brain: Action completed: ${action.type}`)
|
||||
|
||||
const id = action.id
|
||||
if (id)
|
||||
this.inFlightActions.delete(id)
|
||||
this.updatePendingActionsOnBlackboard()
|
||||
this.blackboard.addActionHistoryLine(this.formatActionHistoryLine(action, 'success', result))
|
||||
|
||||
if (!action.require_feedback)
|
||||
return
|
||||
|
||||
await this.enqueueEvent(bot, {
|
||||
type: 'feedback',
|
||||
payload: {
|
||||
@@ -113,6 +136,13 @@ export class Brain {
|
||||
|
||||
this.deps.taskExecutor.on('action:failed', async ({ action, error }) => {
|
||||
this.log('WARN', `Brain: Action failed: ${action.type}`, { error })
|
||||
|
||||
const id = action.id
|
||||
if (id)
|
||||
this.inFlightActions.delete(id)
|
||||
this.updatePendingActionsOnBlackboard()
|
||||
this.blackboard.addActionHistoryLine(this.formatActionHistoryLine(action, 'failure', undefined, error))
|
||||
|
||||
await this.enqueueEvent(bot, {
|
||||
type: 'feedback',
|
||||
payload: {
|
||||
@@ -137,7 +167,18 @@ export class Brain {
|
||||
this.queue.push({ event, resolve, reject })
|
||||
this.log('DEBUG', `Brain: Queue length now: ${this.queue.length}`)
|
||||
this.updateDebugState()
|
||||
this.processQueue(bot)
|
||||
|
||||
if (event.type === 'feedback' && this.feedbackDebounceMs > 0) {
|
||||
if (this.feedbackDebounceTimer)
|
||||
clearTimeout(this.feedbackDebounceTimer)
|
||||
this.feedbackDebounceTimer = setTimeout(() => {
|
||||
this.feedbackDebounceTimer = undefined
|
||||
void this.processQueue(bot)
|
||||
}, this.feedbackDebounceMs)
|
||||
return
|
||||
}
|
||||
|
||||
void this.processQueue(bot)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -157,8 +198,14 @@ export class Brain {
|
||||
this.log('DEBUG', `Brain: Processing event type=${item.event.type}`)
|
||||
this.updateDebugState(item.event)
|
||||
|
||||
// Coalesce consecutive feedback events into a single LLM turn.
|
||||
// This prevents the LLM from being spammed with partial results while still supporting streaming replans.
|
||||
const coalescedEvent = item.event.type === 'feedback'
|
||||
? this.coalesceFeedbackEvents(item.event)
|
||||
: item.event
|
||||
|
||||
try {
|
||||
await this.processEvent(bot, item.event)
|
||||
await this.processEvent(bot, coalescedEvent)
|
||||
item.resolve()
|
||||
}
|
||||
catch (err) {
|
||||
@@ -185,9 +232,20 @@ export class Brain {
|
||||
return `Perception [${signal.type}]${sourceInfo}: ${signal.description}`
|
||||
}
|
||||
case 'feedback': {
|
||||
const { status, action, result, error } = event.payload
|
||||
const payload = event.payload as any
|
||||
if (payload?.status === 'batch' && Array.isArray(payload.feedbacks)) {
|
||||
return `Internal Feedback (batched): ${JSON.stringify(payload.feedbacks)}`
|
||||
}
|
||||
|
||||
const { status, action, result, error } = payload
|
||||
const actionCtx = action
|
||||
? { type: action.type, ...(action.type === 'physical' ? { tool: action.step.tool, params: action.step.params } : { message: action.message }) }
|
||||
? {
|
||||
id: action.id,
|
||||
type: action.type,
|
||||
...(action.type === 'sequential' || action.type === 'parallel'
|
||||
? { tool: action.step.tool, params: action.step.params }
|
||||
: { message: action.message }),
|
||||
}
|
||||
: undefined
|
||||
return `Internal Feedback: ${status}. Last Action: ${JSON.stringify(actionCtx)}. Result: ${JSON.stringify(result || error)}`
|
||||
}
|
||||
@@ -196,6 +254,64 @@ export class Brain {
|
||||
}
|
||||
}
|
||||
|
||||
private coalesceFeedbackEvents(first: BotEvent): BotEvent {
|
||||
const feedbacks: any[] = [first.payload]
|
||||
|
||||
while (this.queue.length > 0 && this.queue[0]?.event.type === 'feedback') {
|
||||
const next = this.queue.shift()!
|
||||
feedbacks.push(next.event.payload)
|
||||
next.resolve()
|
||||
}
|
||||
|
||||
if (feedbacks.length === 1)
|
||||
return first
|
||||
|
||||
return {
|
||||
type: 'feedback',
|
||||
payload: {
|
||||
status: 'batch',
|
||||
feedbacks,
|
||||
},
|
||||
source: first.source,
|
||||
timestamp: first.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
private ensureActionIds(actions: ActionInstruction[]): ActionInstruction[] {
|
||||
return actions.map((action) => {
|
||||
if (action.id)
|
||||
return action
|
||||
return {
|
||||
...action,
|
||||
id: `a${this.nextActionId++}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updatePendingActionsOnBlackboard(): void {
|
||||
const pending = [...this.inFlightActions.values()].map(a => this.formatPendingActionLine(a))
|
||||
this.blackboard.setPendingActions(pending)
|
||||
}
|
||||
|
||||
private formatPendingActionLine(action: ActionInstruction): string {
|
||||
if (action.type === 'chat')
|
||||
return `${action.id ?? '?'} chat: ${action.message}`
|
||||
return `${action.id ?? '?'} ${action.type}: ${action.step.tool} ${JSON.stringify(action.step.params ?? {})}`
|
||||
}
|
||||
|
||||
private formatActionHistoryLine(
|
||||
action: ActionInstruction,
|
||||
status: 'success' | 'failure',
|
||||
result?: unknown,
|
||||
error?: unknown,
|
||||
): string {
|
||||
const base = this.formatPendingActionLine(action)
|
||||
const suffix = status === 'success'
|
||||
? `=> ok ${result ? JSON.stringify(result) : ''}`
|
||||
: `=> failed ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
||||
return `${base} ${suffix}`
|
||||
}
|
||||
|
||||
private async processEvent(bot: MineflayerWithAgents, event: BotEvent): Promise<void> {
|
||||
// OODA Loop: Observe -> Orient -> Decide -> Act
|
||||
|
||||
@@ -230,8 +346,10 @@ export class Brain {
|
||||
|
||||
// Issue Actions
|
||||
if (decision.actions && decision.actions.length > 0) {
|
||||
const actionsWithIds = this.ensureActionIds(decision.actions)
|
||||
|
||||
// Record own chat actions to memory
|
||||
for (const action of decision.actions) {
|
||||
for (const action of actionsWithIds) {
|
||||
if (action.type === 'chat') {
|
||||
this.blackboard.addChatMessage({
|
||||
sender: config.bot.username || '[Me]',
|
||||
@@ -241,7 +359,7 @@ export class Brain {
|
||||
}
|
||||
}
|
||||
|
||||
this.deps.taskExecutor.executeActions(decision.actions)
|
||||
this.deps.taskExecutor.executeActions(actionsWithIds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export function generateBrainSystemPrompt(
|
||||
return {
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
execution: a.execution,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,7 +32,9 @@ Available Actions:
|
||||
${availableActionsJson}
|
||||
|
||||
Rules:
|
||||
1. You can execute physical actions or chat actions
|
||||
1. You can execute sequential actions, parallel actions, or chat actions
|
||||
1.1. Sequential actions are executed strictly in order.
|
||||
1.2. Parallel actions are parallel-safe and can be used for fast information gathering.
|
||||
2. The output must be valid JSON following the schema below
|
||||
3. Specify if a feedback is required for the action, i.e. whether you need to know the execution result for a good reason
|
||||
4. Failed actions will always result in a feedback
|
||||
@@ -48,7 +51,8 @@ Output format:
|
||||
},
|
||||
"actions": [
|
||||
{"type":"chat","message":"...","require_feedback": false},
|
||||
{"type":"physical","step":{"tool":"action name","params":{...}},"require_feedback": false}
|
||||
{"type":"parallel","step":{"tool":"action name","params":{...}},"require_feedback": true},
|
||||
{"type":"sequential","step":{"tool":"action name","params":{...}},"require_feedback": false}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -61,6 +65,13 @@ Strategy: "${blackboard.strategy}"
|
||||
Self: ${blackboard.selfSummary}
|
||||
Environment: ${blackboard.environmentSummary}
|
||||
|
||||
# Execution State (IMPORTANT)
|
||||
Pending actions (started and still running):
|
||||
${blackboard.pendingActions.map(a => `- ${a}`).join('\n') || '- none'}
|
||||
|
||||
Recent action results (most recent last):
|
||||
${blackboard.recentActionHistory.map(a => `- ${a}`).join('\n') || '- none'}
|
||||
|
||||
# Chat History (Recents):
|
||||
${blackboard.chatHistory.map(msg => `- ${msg.sender}: ${msg.content}`).join('\n') || 'No recent messages.'}
|
||||
`
|
||||
|
||||
@@ -4,9 +4,12 @@ import type { Mineflayer } from './core'
|
||||
|
||||
type ActionResult = string | Promise<string>
|
||||
|
||||
export type ActionExecutionMode = 'sequential' | 'parallel'
|
||||
|
||||
export interface Action {
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly execution: ActionExecutionMode
|
||||
readonly schema: z.ZodObject<any>
|
||||
readonly perform: (mineflayer: Mineflayer) => (...args: any[]) => ActionResult
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user