Taos Engine · Agent Tutorials

Agent Tutorials · Step 9

Defining Your Own Model

▶ Live demo: run this tutorial — built from src/09_custom_models.ts. Distilled from samples/agent_termites.ts.

A model in Taos Engine is just a function that returns an AgentModelConfig — there's no class to extend, no plugin, no registration step (unlike AgentScript's class extends Model). To show the whole surface, we build NetLogo "Termites" from scratch: termites carry wood chips, drop them onto existing piles (or rarely seed a new one), and pick up loose chips — and the chips self-organize into a few big piles.

The whole spec#

TypeScript
interface AgentModelConfig {
  world: { min: [number, number]; size: [number, number];
           topology?: 'torus' | 'bounded' | 'bounce' | 'die' };
  population: number;                  // sizes the GPU buffer
  movement?: 'position' | 'velocity';  // default 'position'
  spawn?: AgentSpawn;                  // region, color, size, on-spawn actions
  patches?: PatchConfig;               // a diffusing grid (tutorial 3)
  neighbors?: SpatialHashConfig;       // enables neighbor queries (tutorial 4)
  links?: LinksConfig;                 // link slots (tutorial 7)
  reproduction?: boolean;              // GPU births/deaths (tutorial 6)
  step?: Action[];                     // the per-agent program
  behaviors?: ModifierNode[];          // built-in neighbor/physics behaviors
  render?: { shape?, blendMode?, billboard?, sizeMul?, emit?, tiltToSurface? };
  integrateVelocity?: boolean;         // false → free `velocity` as a render channel
}

You compose the pieces from earlier tutorials. Termites need: a static patch field (chips), state in color.a (carrying vs. searching), and branching.

The termites model#

State machine in color.a (> 0.7 = carrying), reading the chip density here:

TypeScript
const carrying = expr.gt(expr.attribute('alpha'), 0.7);
const here = agent.sense(0, 'bilinear');

step: [
  action.if_(carrying,
    [ // carrying: wander; drop onto a pile (or rarely seed one)
      agent.turn(agent.randomTurn(0.5)),
      action.if_(expr.or(expr.gt(here, 1.5), expr.lt(expr.random({mode:'perFrame',seed:11}), 0.01)),
        [agent.deposit(0, 1), action.set('alpha', 0.4), reorient(21)]),
    ],
    [ // searching: wander; pick up a chip you're standing on
      agent.turn(agent.randomTurn(0.5)),
      action.if_(expr.gt(here, 0.5), [agent.deposit(0, -1), action.set('alpha', 1.0), reorient(31)]),
    ],
  ),
  agent.moveForward(9),
  action.set('rgb', expr.select(carrying, expr.constVec3([1.4,0.8,0.25]), expr.constVec3([0.25,0.35,0.5]))),
],

Everyone starts carrying (spawn.color alpha 1.0); the chip field starts empty and fills via the rare seed drops, then piles snowball because carriers drop onto existing density. Give it ~30 s and a handful of big amber piles emerge — emergent sorting from a three-line rule, the NetLogo classic, at 30k agents.

Where the boundaries are#

The step expresses arbitrary per-agent math, branching, sensing, depositing, and the neighbor/distance verbs. What it can't do (by design, for the fixed-width GPU model) is spawn agents directly (use reproduction + a behavior) or iterate a variable-length link list. New neighbor/physics behaviors are added as ModifierNode variants in the engine — see the module guide.

Next#