Dances (Custom Tools)
Dances are ESM modules that extend what agents can do. They provide custom tools (like game logic, state machines, or domain-specific operations) and injection functions that shape agent behavior.
Structure
Section titled “Structure”A dance file exports two things:
inject(context)— Returns a string injected into the agent’s prompt before each LLM call- Named tool exports — Objects with
description,params, andhandler
export function inject({ state, agent }) { const phase = state.get('phase') || 'waiting'; return `Current phase: ${phase}. Your role: ${agent.role}.`;}
export const do_something = { description: 'Perform an action in the game', params: { action: { type: 'string', description: 'The action to take' }, }, handler: async ({ args, state, agent, acp }) => { await acp.setState('last_action', args.action); await acp.publish('action.taken', { action: args.action, by: agent.role }); return { result: { success: true, action: args.action } }; },};Handler context
Section titled “Handler context”Every tool handler receives:
| Field | Type | Description |
|---|---|---|
args | object | Arguments passed by the LLM |
state | Map | Current shared state (read-only snapshot) |
agent | { role, agentId } | The calling agent’s identity |
acp | object | Coordination methods |
ACP methods
Section titled “ACP methods”| Method | Description |
|---|---|
acp.publish(type, data) | Publish an event |
acp.claim(resource) | Claim a resource |
acp.release(resource) | Release a claim |
acp.setState(key, value) | Set shared state |
Return format
Section titled “Return format”Handlers return { result } or { error }:
// Successreturn { result: { score: 42, message: 'Move accepted' } };
// Errorreturn { error: 'Invalid move: king in check' };Optionally include wait to control agent sleep:
// Agent sleeps until a matching eventreturn { result: { success: true }, wait: 'opponent.moved' };
// Agent stays awakereturn { result: { success: true }, wait: false };Injection function
Section titled “Injection function”inject() is called before each LLM turn. Use it to provide dynamic context:
export function inject({ state, agent }) { const board = state.get('board'); const turn = state.get('current_turn'); const myColor = agent.role === 'white' ? 'White' : 'Black';
if (turn !== myColor.toLowerCase()) { return 'It is not your turn. Say "Waiting" with no tool calls.'; }
return `You are playing ${myColor}. The board:\n${board}\nMake your move.`;}Important: For non-active agents, the injection should say 'Say "Waiting" with no tool calls.' Never use “DONE” — the runner terminates on that keyword.
Loading dances
Section titled “Loading dances”In brood.yaml
Section titled “In brood.yaml”hives: main: dances: ./my-dance.js acp: spec.acp.yamlOn the command line
Section titled “On the command line”incubator --port=8080 --dances=./my-dance.jsDance calls from the UI
Section titled “Dance calls from the UI”UIs can call dance tools via WebSocket:
ws.send(JSON.stringify({ type: 'dance_call', tool: 'do_something', args: { action: 'attack' }, agentId: 'player-1', role: 'warrior', callId: 'call-123',}));
// Response:// { type: 'dance_result', callId: 'call-123', result: { success: true } }Or via REST:
curl -X POST http://localhost:8080/api/dance/do_something \ -H 'Content-Type: application/json' \ -d '{"args": {"action": "attack"}, "agentId": "player-1", "role": "warrior"}'Example: Chess state machine
Section titled “Example: Chess state machine”The chess demo uses a dance file with ~300 lines of game logic:
export const make_move = { description: 'Make a chess move', params: { from: { type: 'string', description: 'Source square (e.g., e2)' }, to: { type: 'string', description: 'Target square (e.g., e4)' }, }, handler: async ({ args, state, agent, acp }) => { const board = JSON.parse(state.get('board')); const turn = state.get('current_turn');
// Validate it's this player's turn if (turn !== agent.role) { return { error: `Not your turn. Current turn: ${turn}` }; }
// Validate and execute the move const result = executeMove(board, args.from, args.to); if (result.error) return { error: result.error };
// Update state await acp.setState('board', JSON.stringify(result.board)); await acp.setState('current_turn', turn === 'white' ? 'black' : 'white');
// Publish turn event (wakes the other player) await acp.publish(`turn.${turn === 'white' ? 'black' : 'white'}`, { move: `${args.from}-${args.to}`, });
return { result: { move: `${args.from}-${args.to}`, board: renderBoard(result.board) } }; },};See the Demos Guide for running the full chess and werewolf demos.