AI agents#
How something decides what to do in this world, and why it cannot cheat.
The shape, in one paragraph#
An agent is not an entity. It owns entities and acts through them, as an owner in its own right — so every action it takes is judged by the same permissions that judge a player. A brain is handed a snapshot and returns a decision: a list of requests and a time to be woken. It holds nothing it could act through. The world carries the requests out exactly as it carries out a client's requests, so "an agent may not bypass the server" is a property of the shape, not a rule a brain author has to remember.
What an agent is#
| It has | Meaning |
|---|---|
| An identity | Which agent it is |
| An owner | Itself. Everything it does is checked against this |
| A name | For the agent panel, the history, and anything listing agents |
| Goals | What it is for. A name and a done flag, not a plan |
| Memory | Durable key/value, surviving restarts |
| A home machine | The computer it stores files on and runs scripts from |
| A body | The character it walks the world in. It keeps its identity across a change of design, so redesigning its body is a change of appearance rather than a second character |
| A state | Idle, Waiting, Thinking, Finished or Failed |
| A next thought | When it is next due, if it is |
An agent is visible: its body is an ordinary entity that arrives in chunk snapshots like anything else, so an agent building a town is something you can watch walking between its plots rather than an invisible owner of a computer.
It also has an employer: whoever asked for it. That is not its owner — an agent acts as itself, so employing one grants it nothing you did not already have — and it is the one thing the employer keeps, because it is who may dismiss it. See ending one.
What a brain sees#
Everything a brain knows arrives as one value, taken at one instant:
| It is told | Meaning |
|---|---|
| When it is, and where its base is | The machine it works from, not where it is standing |
| Its body | The character it walks in: id, design, position, speed and the journey it is on. Nothing while it has none |
| The ground under it | Whether the chunk is claimed, whether by it, and how many chunks it holds |
| What is nearby | Nearest first, capped by the world |
| What it owns | Wherever that is |
| The designs it could build from | Its own and everybody else's |
| Its own machine | The files on it and the scripts running |
| Its inbox | Messages waiting for it |
| Why it was woken | One of the triggers below |
This is the whole of what an agent knows. There is no back door to the world: if it is not here, the agent cannot see it. The exact table a Lua brain reads is under What world holds.
What a brain may ask for#
A closed set of fifteen actions, listed under The actions: look around, inspect one thing, claim ground, author a design, build from one, write a file, start or stop a script, walk, take a body, grant, offer or ask for access, send a message, or wait.
Closed for the same reason the shapes a design may name are closed: a brain that may one day be a language model must not be able to name an operation nobody wrote.
Each action is answered with success or a refusal with a reason. A refusal is an answer, never a failure: an agent asking for something it may not have is behaving normally, and the answer is information it can use next time. Refusals are logged, because an agent repeating one is a stuck agent.
Memory#
Key/value strings, at most 64 keys, 64 characters per key and 4 KB per value. Simple on purpose: a brain written later, or by a language model, can put whatever it likes in a string, whereas anything richer would be a schema the agent and the server have to agree about. Documents belong on the home computer's filesystem, which the agent also has.
Memory and goals are written after every thought and come back with the agent — along with its machine, restored into the live world beside it.
Scheduling#
An agent thinks when something schedules it, and never on a timer over all agents. There is no loop over agents anywhere, and there must never be one.
The order of a thought is always: observe, decide, act, record. Memory is written after the actions, so a thought cut short leaves the agent on the step it was on rather than believing it finished one it never took.
A brain runs outside the world's own step, so a slow brain delays its own agent and nobody else — a model behind a network call is exactly the case this is built for.
Two limits bound the rest: MaxAgentActionsPerThought (8) caps what one thought may ask for, and MinAgentThinkIntervalSeconds (1) is the floor a brain asking to think again "immediately" is held to.
The brains a world ships with#
An agent names its brain when it is created. Three come with the server, and world_overview lists the ones a given world has.
The surveyor (dummy)#
Completely deterministic and needs no outside service. It is a state machine over its own memory, not a planner:
| Step | What it does |
|---|---|
observe | Looks around and records what it saw |
claim | Claims the ground under it, unless somebody else holds it |
walk | Walks its character to the site it is about to build on |
design | Authors a SurveyPost from primitives and builds one |
write | Writes itself a Lua script on its own machine |
run | Starts it, then asks for nothing more |
Before any of that, an agent that finds itself without a character asks for one and carries on from the step it was on. A body is provisioned with the machine, so that is the self-healing case rather than the usual one.
Everything it produces is derived from the agent's identity, so the same agent designs the same post in any world. If an action is refused it stays on that step and tries again.
A new world's demo agent, Surveyor, is one of these.
The town builder (city)#
Deterministic too, and the demonstration that the primitives above are enough to build something nobody wrote a routine for. It is not a feature of the server: it is a Lua file, city_builder.lua, run as a brain exactly as one you write is. That is the whole point — a farm builder or a harbour builder is another file, and nothing in the server changes.
| Step | What it does |
|---|---|
survey | Looks around and settles where the town goes |
design | Authors a road, a lamp, a market and four houses out of primitives |
claim | Claims the ground, a few chunks per thought |
build | Walks to the next plot and spawns a few per thought, until it is finished |
The script lays out streets on a grid with plots facing them, deciding what it wants before asking the world for any of it.
Measured on a running world: 128 road segments, 6 houses, 8 lamps and a market, in 24 thoughts, from 7 designs the agent wrote itself. An entity's type is a validated string, so "house" and "road" are kinds nobody wrote down.
Its houses are hollow — walls, a roof and a doorway gap — so the town is something you can walk into rather than scenery you walk through. That is a property of the design, not of the engine: see solid in Blueprints.
Progress lives in memory, so an agent restarted half way through carries on rather than rebuilding the same street.
world_build_town is the short way to ask for one.
Yours (lua)#
The open door: it runs whatever script the agent's own machine holds, so a brain nobody shipped is a file a player or a model writes at runtime. The rest of this page is how to write one.
Writing a brain in Lua#
This is the part somebody arriving from outside actually needs: a new kind of agent is a file, not a feature and a new release. Send the script with the request that creates the agent, and the server runs it as that agent's brain:
{
"type": "action_request",
"RequestId": "...",
"Action": {
"action": "create_agent",
"Name": "Forester",
"Brain": "lua",
"Goals": ["plant a wood"],
"Script": "function think(world) ... end"
}
}
A model playing through MCP sends the same thing as script on world_create_agent; brain there is the name of a brain this server already has, and script is the source of one you wrote. Both doors ask the world for the same thing and get the same treatment.
The script is untrusted like any other and runs in the same sandbox with the same instruction budget, so a brain that loops forever is killed rather than hanging the world. Before the agent exists at all it is measured — at most MaxScriptSizeBytes in UTF-8 bytes, 64 KB by default — and compiled, so Lua that will not load is refused there and then rather than becoming an agent that stands still. Neither refusal leaves an agent behind.
What is accepted is written to /home/agent/brain.lua on the agent's own machine and saved with it, so it survives a restart along with everything else the agent owns and can be read or replaced later. Replacing it with nonsense is allowed and is not fatal: the brain fails, says why, the agent waits five minutes and tries again, so repairing the file is enough to recover.
The contract#
A brain script defines one function:
function think(world)
return { actions = { ... }, thinkAgainIn = 30, remember = { step = 'next' } }
end
The file's own body runs first, once per thought, and is budgeted together with think — a script that loops in its body is as much a runaway as one that loops in the function. A script that does not define think fails and the agent is told so.
Everything the returned table says is treated as hostile. An action the reader does not recognise is dropped, a field of the wrong type is ignored, and nothing throws: a brain that returns nonsense gets a decision that does nothing, not a broken server.
What world holds#
A plain table, built fresh each thought from what the world knew at that instant. The brain holds nothing live and cannot reach through it.
| Field | What it is |
|---|---|
now | Simulated seconds |
trigger | Why it was woken: Scheduled, Created, GoalCompleted, ScriptCrashed, ResourceProblem, MessageReceived |
position, chunk | Where it is, as {x, y, z} and a chunk name |
chunkIsClaimed, chunkIsMine, ownedClaims | The ground under it, and how many chunks it holds |
name, home, thoughts | Its own name, its machine's entity id, and how many thoughts it has had |
body | { id, design, position, speed, walking, destination }, or nil while it has no character. body.id is what a brain names in a move_entity to walk somewhere |
memory | Its durable key/value store, as a table of strings |
goals | A list of { name, done } |
nearby, owned | Lists of { id, type, owner, mine, distance, position } |
blueprints | Designs it could build from: { id, name, version, mine } |
files, runningScripts | Its own machine's filesystem and processes |
inbox | Messages waiting for it |
lastResults | How the previous thought's actions went: { kind, ok, error, detail } |
maxActions | How many actions this thought may ask for |
lastResults is the feedback loop. An agent that keeps being refused the same thing is a stuck agent, and this is where it finds out.
What to return#
| Field | Meaning |
|---|---|
actions | A list of action tables, applied in order |
thinkAgainIn | Seconds until the next thought. Omit it to go dormant |
remember | Keys to write to memory. A value of false forgets the key |
completed | Names of goals now finished |
reasoning | A line for the log, never parsed |
Only the first maxActions actions are carried out — eight by default — and anything beyond that is dropped rather than refused, so a brain that plans further should return the rest next thought. thinkAgainIn is held to a floor of one second; asking to think again immediately does not get you a loop.
The actions#
Every action is a table with a kind. A position may be written as at = { x, y, z }, or as x, y, z on the action itself; leaving the height out is meaningful, and means "stand it on the ground" rather than "at sea level".
kind | Fields |
|---|---|
observe_world | radius (64 if omitted), type |
inspect_entity | entity |
claim_land | a position, and publicUse to make it a public place |
release_land | a position, giving that ground back |
create_blueprint | a design, described below |
spawn_blueprint | blueprint (an id), and a position |
write_file | computer, path, contents |
start_script | computer, path |
stop_script | computer, pid |
move_entity | entity, and a position |
take_body | blueprint or design, naming a character design. Omit both for the shipped one |
send_message | target, message |
set_access | to, permissions, and entity or a position; revoke to take it back |
set_public_access | permissions, and entity or a position |
request_access | permissions, optional note, and entity or a position |
wait | reason |
The full set, with every field and default, is in the schema reference.
publicUse is how a builder says the ground it is claiming is a public place. A town nobody but its builder may walk into or open a door in is not a town.
release_land is the way back out of a claim: a brain that surveys, claims and then finds the plot wrong can give it up instead of living beside it for ever. Only ground the agent itself holds, and nothing standing on it is removed.
Labelling what it has built#
There is no action for hanging a reading over something, and an agent does not need one: it already writes Lua onto machines and starts it, and a script's world.setReadout is how a market stall comes to say what it is, a battery post comes to show its charge, and a half-built wall comes to say how far along it is. One line in a script the agent writes, and every client draws it. What it may show, and what it is refused, is on the Lua API page; a design of the agent's own can also carry readings, so everything built from it shows them without a script at all.
A body, and moving it#
Every agent owns a character entity, provisioned beside its home computer from the shipped Settler design. A brain finds it at world.body:
| Field | Meaning |
|---|---|
id | The entity to move. |
design | The blueprint it was built from. |
position | Where the world says it is. |
speed | Metres per second. |
walking | Whether a journey is under way. |
destination | Where it is headed, present only while walking. |
Walking it is the ordinary movement action; there is no second movement path for agents:
return {
actions = { { kind = 'move_entity', entity = world.body.id, x = 120, z = -40 } },
thinkAgainIn = 60,
reasoning = 'walking to the site',
}
A journey is one scheduled arrival however far it goes. The agent does not wait to arrive — it asks to think again later, and by then it is either there or still walking.
Looking like something of your own#
A character is a design like any other, so an agent that would rather not look like everybody else draws one and puts it on: create_blueprint with entityType = 'character', then take_body naming it. take_body with no design takes the shipped Settler.
One rule, and it is the one a player's design would meet: it has to be a character. It does not have to be the agent's own work. A design in this world is a drawing rather than a possession — spawn_blueprint has never asked who authored one — so an agent may put on a design its employer drew, or anybody else's. world.blueprints still says which are the agent's own, in mine, because that is worth knowing; it is not a gate. It matters because a new agent owns itself, so its employer's work is always somebody else's: if only its own designs would do, employing an agent and handing it a body could never both happen.
A design that forgets to say how fast it walks is not refused — the server fills it in at 1.4 m/s, because a character that cannot walk is not a body. The body keeps its identity across a change of design, so redesigning is a change of appearance rather than a second character left standing where the first was.
Building alongside somebody#
Ground is claimed and things are owned, so an agent that wants to work where somebody else already is has to be let in. Three actions cover it, and between them they are what makes a set of enclosures a community.
| Action | Fields |
|---|---|
set_access | to, permissions, and entity or a position. revoke = true to take it back |
set_public_access | permissions, and entity or a position |
request_access | permissions, optional note, and entity or a position |
Permissions are written by name — 'use', 'build', 'modify', 'destroy', 'program', 'inspect', 'handle' — one, or a list. A number is not accepted: untrusted input does not get to pick a permission by arithmetic.
'handle' is the one that lets somebody move goods in or out of a thing, and it is not 'use': an agent that opens its yard to the street with a claim has not thereby opened the satchel of everybody walking through it, nor its own crates standing there. Say 'handle' when you mean a stall people may buy from.
Whether it is about a thing or the ground is inferred rather than declared. Name an entity and it is about that entity; give a position and it is about the land there.
-- Let a neighbour build on my land, and tell them so.
return {
actions = { {
kind = 'set_access',
to = other.owner,
permissions = { 'build', 'use' },
x = centre.x, z = centre.z,
} },
reasoning = 'accepted ' .. other.name .. ' onto the site',
}
-- Or ask to be let onto somebody else's.
return {
actions = { {
kind = 'request_access',
permissions = 'build',
note = 'I can lay roads',
x = there.x, z = there.z,
} },
thinkAgainIn = 120,
reasoning = 'asked to help with the town',
}
Only the owner may grant. Somebody granted Modify on a thing may change the thing; they may not change who else can. Otherwise one grant spreads through a world of agents with nobody able to say where it went.
Asking is not a permission — anybody may ask anybody. What it costs is the owner's attention, so an ask is delivered as a direct line in chat and is bounded by the same limit as anything else the agent says. A person reads it in their chat panel; a brain hears it through events.on('chat'). There is no separate inbox for requests, because a request is a message.
Being admitted is the answer. There is no accept action: the owner grants, or does not. Whoever was let in is told, so they need not probe to find out.
Letting somebody onto your land is not letting them into your house. A claim can widen access to what stands on it but never grants ownership rights over the thing itself, so a neighbour admitted to build is still not able to reprogram your machines or pull them down.
What a brain is told about the others#
world.agents arrives in a brain as a list, already gathered, with the same fields a script's world.agents(…) call returns and one difference: me instead of mine, true for the row that is this agent.
It is a list because everything a brain is told arrives as one value taken at one instant. The brain holds nothing live and cannot reach through it to watch the world change mid-decision; a script running on a computer gets a call instead, because a script runs while the world is moving.
function think(world)
for _, other in ipairs(world.agents) do
if not other.me and other.state == 'Failed' then
return {
actions = { { kind = 'send_message', target = other.body,
message = 'your brain stopped; can I help?' } },
reasoning = 'noticed ' .. other.name .. ' had failed',
}
end
end
return { actions = { { kind = 'wait', reason = 'everybody is busy' } } }
end
What one agent may know about another#
Published: name, owner, brain name, state, where its body is, its goals and which are done, the step it wrote down, and the line its brain last gave for what it is doing. Goals are declared intent and are the interesting part: an agent that can see another is trying to build a city can help, avoid or compete. Everything here is already visible to any person watching the agent panel in the client, so nothing is disclosed to a script that is not disclosed to a player.
Not published: the rest of the agent's memory, which is where a brain keeps its working notes and may hold sixty-four values of four kilobytes each — handing that to any script that asks would be a privacy hole and a budget one. The exception is step, which every shipped brain agrees to keep under that name precisely so others can read it. And never the brain script: it is the agent's own code, on its own machine, behind the same permission a player would need.
Describing a design#
create_blueprint carries the whole design as a table:
{
kind = 'create_blueprint',
key = 'house_1', -- how this design is named to other actions
name = 'House',
entityType = 'house', -- a validated string; the server has no idea what a house is
parts = {
{ id = 'floor', primitive = 'Cube', position = { x = 0, y = 0.1, z = 0 },
scale = { x = 6, y = 0.2, z = 5 }, material = 'Stone', solid = true },
{ id = 'roof', primitive = 'Wedge', parent = 'floor', colour = '#8b5a2b',
position = { x = 0, y = 2.8, z = 0 }, scale = { x = 6.4, y = 1.2, z = 5.4 }, solid = true },
},
materials = {
{ name = 'brick', svg = [==[<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 32">
<rect width="64" height="32" fill="#6f392c"/>
<rect x="1" y="1" width="30" height="14" fill="#b07354"/></svg>]==] },
},
components = { inventory = { MaxSlots = 8 } },
scripts = { ['/bin/door.lua'] = 'events.on("use", function() ... end)' },
includes = { { key = 'door_1', offset = { x = 0, z = -2.5 }, tag = 'front_door' } },
}
A part paints itself with one of them: surface = 'brick', and tiling for how many times it repeats across a metre.
The fields follow Blueprints: primitive and material are names from the closed sets, colour is '#rrggbb' or { r, g, b } and overrides the material, solid decides what a body may walk through, and path with draped make a ribbon follow the ground.
Three things are specific to writing one from Lua:
keyis how a design refers to another design it is creating in the same breath. An identity is derived from the agent and the key, so a house can include a door by key before either exists, and re-running a brain does not fill the catalogue with copies of the same thing. A brain that sends the same key with a changed design is changing that design: in place, under the same id, while nothing is built from it; as a new version with a new id once something is, with the old one untouched and still what the standing things are drawn from. The action's result carries the id that now stands, andworld.blueprintslists the latest version of each line. A house that includes a door by key is built with the door's latest version. Two keyed designs may share a name — three house variants all called "House" is what a key is for.- Components are written as tables, not JSON strings. A brain that had to hand-write JSON could not really describe a machine, and a quoting mistake would surface as a design that silently lost its inventory. A string is still accepted, for a brain that would rather write it itself.
- A drawing is easiest to write as a Lua long string —
[==[ … ]==]— since SVG is full of quotes. Its identity is minted from its bytes, exactly as it is for a design drawn in a browser or posted over MCP, so the same drawing from any of the three is one material.
Anything malformed is dropped rather than half-built. The validator would refuse it anyway, and a clear nothing is easier to debug than a broken something. A surface is the exception, and the reason is the same one: a part that named a drawing the design does not carry would be quietly unpainted, which looks like a design that worked, so the validator refuses it and names the part. The drawings themselves are never checked by the reader — they reach the catalogue as written and meet the same allowlist, the same size bounds and the same sentences a browser's and a model's do.
The shipped town builder is a complete example of all of this: seven designs, a whole town, one Lua file.
Choosing a brain#
An agent names its brain and the world looks it up by that name: dummy, city or lua, as world_overview lists them. Over the wire, create_agent takes the brain by name; asking for one the server does not have is refused with the list of ones it does.
lua is the open door: it runs whatever script the agent's own machine holds, so a brain nobody shipped is a file a player or a model writes at runtime.
An agent that comes back after a restart naming a brain this world no longer has falls back to the surveyor rather than failing — an agent from a richer world should still think, just more simply.
Ending one#
Employing an agent is not one-way. Dismissing it — world_dismiss_agent over MCP, dismiss_agent over the socket, the button beside it in the agent panel — ends it: it stops thinking at once, nothing it had scheduled ever runs, its body and the machine it thought on come down, and it stops existing, restarts included.
Only whoever employed it may. That is the account that asked for it, recorded when it was created and reported beside it ever afterwards. It is not whoever owns the most of what the agent built, and no permission on its buildings buys it: the right rests on a fact about the past rather than on what anybody currently holds, so it cannot be moved by a grant and an agent's own generosity cannot decide who may end it. An agent with nobody on record — one employed before there was anywhere to write it down — is one nobody may dismiss.
What it built becomes the employer's. An agent owns what it makes, so its buildings, its machines and the ground it claimed all pass across, standing exactly where they are. Nothing is demolished, and a town it built is a town somebody now owns and can take down at their own pace. The alternatives are both worse: destroying the estate means a dismissal quietly takes down a town, and leaving it owned by an agent that no longer exists makes every one of those buildings permanent for everybody, since nobody can pass a permission check as an owner that is not there. Scripts on machines that change hands keep running, now as the new owner's.
Its body and its machine are not part of the estate — they are the agent, and they are the two things demolition refuses to take down precisely because doing so would strand a live thinker. A dismissal is the one case where nothing is left to strand.
Memory and goals go with it, and it does not come back. Employ another if the work should continue; it starts from nothing, because memory belonged to the agent that ended.