Actors and adapters
The adapter contract: observe, actions and apply — and the rules that keep embodiment out of the kernel.
The contract
An adapter is deliberately small. It contains only what is true about a class of embodiment: what can be measured about it, what it can be asked to do, and how to do it once permission has been granted. It contains no policy — no “should”, only “can”.
return Runtime.defineAdapter({
id = "machine/gantry",
-- Normalise the model into the observation the kernel reasons over.
observe = function(model)
return {
boom_angle = model.Boom:GetAttribute("Angle"),
hoist_height = model.Hoist.Position.Y,
payload = model:GetAttribute("PayloadId"),
envelope = { radius = 42, height = 28 },
faults = model:GetAttribute("Faults") or {},
}
end,
-- Everything this embodiment can be asked to attempt.
actions = {
slew = { params = { angle = "number" }, cost = { power = 2 } },
hoist = { params = { height = "number" }, cost = { power = 3 } },
grip = { params = { target = "Instance" } },
release = {},
},
-- Actuation only. Validation and resource settlement already happened.
apply = function(model, action, params)
if action == "slew" then
model.Boom:SetAttribute("TargetAngle", params.angle)
end
end,
})observe
Returns the declared observation fields for one instance. This is an allowlist, not a snapshot: a field you do not return does not exist as far as the planner, the diagnostics and any external provider are concerned.
Practical consequences worth internalising:
- Declaring narrowly is a privacy control, not just a performance one.
- Observations are frozen per tick. A plan is never built against state that moved underneath it.
- Reading wall-clock time, random values or unsynchronised client state inside
observedestroys reproducibility. See Deterministic execution.
actions
Declares every operation this embodiment can be asked to attempt. Each entry names its parameters, its preconditions and its cost against declared resources. The planner composes plans only from this set.
An action is a request to attempt, not a guarantee of success. Preconditions are checked by the validator, not by the adapter, so an adapter never has to decide whether something is allowed.
apply
Runs after validation and resource settlement have already succeeded. By the time apply is called, the question of permission has been answered — the adapter’s only job is to move the model.
If actuation throws, the action is marked failed, the resource transaction is rolled back, and the fault is recorded in diagnostics. An actor never ends up having paid for something that did not happen.
Attachment
Attachment binds a model, an adapter and a role. It fails immediately if the role references a capability the adapter does not declare — a mismatch surfaces at attach time rather than at the first decision, which is usually hours later and much harder to trace.
Rules of thumb
- If the adapter is getting large, the behaviour probably belongs in a domain pack.
- If two adapters share a field name, they should share its meaning too. Consistent observation vocabulary is what lets one role serve several embodiments.
- If an action needs to know the game rules, it is a goal or a constraint, not an action.