01 ยท Quickstart
Two ways in
Play in the browser
Open play.html. No account, no key, no build step โ
the deterministic Mock LLM ships in the box, so every machine works
offline. Add a real API key later in the settings panel if you want live models.
Run headless in Node
The whole engine is plain ES modules with zero dependencies. Clone the repo, save the script below in the repo root, and run it โ a small factory is built inline, fed two inputs, and run to completion:
forge-demo.mjs
// forge-demo.mjs โ run a TokenForge factory headless. No deps, no build step.
// Usage: node forge-demo.mjs (run from the tokenforge/ repo root)
import { runBlueprint } from './src/engine/api.js';
// inbox โ belt โ Prompt Forge (2ร2) โ delivery, all facing East.
const bp = {
v: 1, name: 'shout-machine', w: 12, h: 6,
entities: [
{ t: 'inbox', x: 1, y: 2, r: 1, c: { name: 'in' } },
{ t: 'belt', x: 2, y: 2, r: 1 },
{ t: 'llm', x: 3, y: 2, r: 1, c: { prompt: 'Uppercase this: {{input}}' } },
{ t: 'belt', x: 5, y: 2, r: 1 },
{ t: 'delivery', x: 6, y: 2, r: 1, c: { name: 'out' } },
],
};
const result = await runBlueprint(bp, {
inputs: { in: ['hello world', 'the factory must grow'] },
ticks: 300, // hard cap; stops early once the line settles
seed: 42, // deterministic MockLLM + rng
});
console.log(result.outputs.out); // raw payloads: ['HELLO WORLD', ...]
console.log(result.text.out); // stringified: same, JSON for objects
console.log(result.ticks, 'ticks,', result.deliveries.length, 'deliveries');
$ node forge-demo.mjs
[ 'HELLO WORLD', 'THE FACTORY MUST GROW' ]
[ 'HELLO WORLD', 'THE FACTORY MUST GROW' ]
38 ticks, 2 deliveries
No provider was configured, so the Mock LLM handled the Prompt Forge โ its
uppercase rule matched the prompt. Swap in a real model via the
provider config without touching the blueprint.
02 ยท Headless JS API
src/engine/api.js
One import gives you everything:
import { runBlueprint, createSim, Sim, MockLLM, load, serialize, makePacket, flush }
from './src/engine/api.js';
runBlueprint(bp, opts) โ Promise<result>
Loads a blueprint, feeds queued inputs to inbox
buildings, ticks the sim until it settles (or the tick cap), and collects everything that landed
in delivery buildings.
| Option | Default | Meaning |
|---|---|---|
inputs | {} | Map of inbox name โ array of payloads, e.g. { in: ['a', 'b'] }. Each value is queued for inbox buildings whose config.name matches. |
ticks | 300 | Maximum ticks to run. The run stops early after 30 quiet ticks (settleTicks) with no new deliveries, nothing in flight, and all input queues drained. |
llm | null | An LLM provider (anything with complete(req)). Defaults to a fresh MockLLM. |
seed | 1234 | Seed for the sim's deterministic RNG. |
services | {} | Extra services merged into sim.services. |
settleTicks | 30 | Quiet-tick threshold for early stop. |
The resolved result object:
| Field | Shape | Meaning |
|---|---|---|
outputs | { [name]: payload[] } | Raw payloads per delivery name (strings, numbers, or objects). |
text | { [name]: string[] } | Same as outputs but every payload stringified (objects via JSON.stringify). |
deliveries | [{ tick, name, packet, entId }] | Full delivery records with complete packet objects ({id, type, payload, meta}) โ metadata included. |
ticks | number | How many ticks actually ran (sim.t). |
events | [{ tick, type, ... }] | Rolling event log (last 500): deliver, log, error. |
createSim(bp, {llm, seed, services}) โ Sim
When you want to drive the simulation yourself โ stepping tick by tick, subscribing to
events, feeding inputs mid-run โ build a Sim instead:
import { createSim, MockLLM, flush } from './src/engine/api.js';
const sim = createSim(bp, { llm: new MockLLM(), seed: 7 });
sim.on('deliver', ({ name, packet }) => console.log('deliver:', name, packet.payload));
sim.pushInput('in', 'tick by tick'); // queue a payload for inbox name 'in'
for (let i = 0; i < 20; i++) { sim.tick(); await flush(); }
// ...or simply: await sim.run(20) โ ticks and flushes async jobs for you
sim.deliveries // [{ tick, name, packet, entId }, ...]
sim.outputs('out') // packets delivered under a given name
sim.events // rolling log of deliver/log/error events
Sim essentials:
sim.tick()โ one synchronous step: machines tick โ output buffers drain (1 packet per port per tick) โ belts move. Async work (LLM calls, scripts) runs between ticks; callawait flush()after each tick, or useawait sim.run(n)which does both.sim.pushInput(name, payload)โ queue a payload; the next freeinboxwith thatconfig.nameemits it as a packet.sim.deliveriesโ everythingdeliverybuildings received, in order.sim.on('deliver', fn)โ subscribe to events; also'log'and'error'. Handlers get the event data ({name, packet, ent}for deliveries).sim.place(type, x, y, rot, config)โ add a building programmatically;serialize(sim, name)turns the grid back into blueprint JSON.
MockLLM โ the deterministic provider
src/engine/mockllm.js exports a fully offline "language model" that powers the
game with no key and makes every test reproducible. It implements the same interface as real
providers: await provider.complete(req) โ { text, toolCalls?, usage } where
req is { system?, prompt?, input?, messages?, tools?, temperature?,
maxTokens?, model? }.
How a rule fires: the mock lowercases system + prompt and scans
it for instruction keywords; the text it transforms is req.input if set, else the
last user message, else the prompt itself. Rules are checked in this exact order โ
first match wins:
| Instruction matches | Response |
|---|---|
uppercase / upper case / all caps | Input uppercased. |
lowercase / lower case | Input lowercased. |
reverse | Input with characters reversed. |
pirate | Arr! <input> โ yo ho ho! |
translate โฆ french / in french | Word-by-word translation from a 21-word built-in dictionary (helloโbonjour, worldโmonde, catโchat, โฆ); unknown words pass through. |
emoji | Input plus one emoji, hash-picked from a set of 8 (same input โ same emoji). |
sentiment / positive or negative | positive, negative, or neutral by counting keyword hits from built-in positive/negative word lists. |
classify / categorโฆ / which of the following / choose one of | Looks for Categories: / Options: / Labels: followed by a comma- or pipe-separated list in the prompt, then picks the option with the best word overlap with the input; zero overlap โ deterministic hash pick. (No list found โ falls through to later rules.) |
score/rate/judge/grade plus a range like 0-10 or 1 to 100 | A deterministic hash score 0..10 as text. |
summarโฆ | First sentence of the input, truncated to 12 words with โฆ if longer. |
haiku / poem | A three-line haiku template seeded with the input's first words. |
json / extract | JSON string: { text, words, emails, numbers } โ first 60 chars, word count, and any emails/numbers found in the input. |
name / title | A hash-picked two-word name like Swift Fox or Crimson Anvil. |
| (nothing matched) | [mock] <input> โ a dependable echo so pipelines always keep flowing. |
Tool-call heuristic
When req.tools is non-empty, the mock behaves like a one-shot agent and the rules
above are skipped:
- First round (no
role:'tool'messages yet): it scores every tool by word overlap between the tool'sname + descriptionand the task text, and returns{ text: '', toolCalls: [{ name, args }] }for the best match. Arguments are filled heuristically from the input:number/integerparams get numbers parsed out of the input (in order,0when exhausted); every other param gets the full input string. - After tool results (any
role:'tool'messages present): it answersResult: <tool contents joined by '; '>โ a plain final answer, no further tool calls.
Scripted mode & call log
For exact tests, queue canned responses โ they're consumed in order before any rules run:
const mock = new MockLLM(); // new MockLLM({ latencyMs: 50 }) to simulate latency
mock.script([
{ text: '', toolCalls: [{ name: 'calc', args: { expr: '1+1' } }] },
{ text: 'The answer is 2.' },
]);
// each scripted response is returned as { usage: {in:10, out:10}, ...yourFields }
const result = await runBlueprint(bp, { llm: mock });
mock.calls // every request object the mock received โ assert on prompts, tools, messages
In rules mode, usage is derived from real word counts
({ in: promptWords + 10, out: responseWords }). Once the script queue empties, the
mock falls back to rules.
03 ยท Blueprint format
Blueprint JSON
A blueprint is the serialized factory โ what the game saves, what
runBlueprint loads, and what you POST to the HTTP API:
{
"v": 1, // format version โ must be 1
"name": "shout-machine", // display name
"w": 12, "h": 6, // grid size (defaults 48ร32 when omitted)
"entities": [
{ "t": "inbox", "x": 1, "y": 2, "r": 1, "c": { "name": "in" } },
{ "t": "belt", "x": 2, "y": 2, "r": 1 },
{ "t": "case", "x": 3, "y": 2, "r": 1, "c": { "mode": "upper" } },
{ "t": "delivery", "x": 4, "y": 2, "r": 1, "c": { "name": "out" } }
]
}
Each entity: t = building id (see the reference),
x, y = top-left grid tile, r = rotation (defaults to 1/East),
c = config overrides merged over the building's defaults. Tiles hold at most one
entity; multi-tile buildings (Prompt Forge and Agent Core are 2ร2) occupy their whole
footprint.
Rotation
r encodes the facing direction: 0=N, 1=E, 2=S, 3=W. Port sides
in building definitions are declared for the default facing rot=1 (East) โ
"W in, E out" reads left-to-right โ and the engine rotates them with
the entity. Where a declared side ends up:
| Declared side (at rot=1 E) | r=0 (N) | r=1 (E) | r=2 (S) | r=3 (W) |
|---|---|---|---|---|
E โ front (usually out) | N | E | S | W |
W โ back (usually in) | S | W | N | E |
N โ left flank | W | N | E | S |
S โ right flank | E | S | W | N |
Belts move packets one tile per tick toward their own r. A belt hands into a
machine only if the machine has an in-port on the facing side; machines butt-joined
port-to-port skip belts entirely.
examples/*.json are
blueprints plus top-level title, description, inputs
(sample inputs), and expect (assertions for tests) โ the blueprint fields above are
a strict subset, so they load anywhere a blueprint does.04 ยท HTTP API
Factory as a service
The bundled server serves the game and a JSON API from one process:
node server/serve.js
# โ serving on http://localhost:8123 (set PORT to change)
It serves the repo root as static files with correct MIME types (html, js, css, json, svg,
png), is path-traversal safe, and mounts the API below. All /api/* routes send
Access-Control-Allow-Origin: * and answer OPTIONS preflights, so you
can call them from any web page. Errors come back as JSON โ the server never crashes on bad
input.
/api/healthLiveness probe: version and building count.
$ curl http://localhost:8123/api/health
{"ok":true,"version":"0.1.0","buildings":43}
/api/buildingsThe full machine catalog straight from the engine's registered definitions โ an array of
{ id, label, icon, cat, desc, size, ports, configSchema, defaults, unlockTier }.
Everything the building reference below shows, as JSON.
$ curl -s http://localhost:8123/api/buildings
[{"id":"belt","label":"Belt","icon":"โก๏ธ","cat":"core", ...}, ...]
/api/examplesThe manifest of shipped example factories (examples/index.json). Returns
[] if no manifest is present.
$ curl http://localhost:8123/api/examples
/api/runRun a factory server-side with runBlueprint. The body takes either an
inline blueprint or the filename of a shipped example:
| Body field | Type | Meaning |
|---|---|---|
blueprint | object | An inline {v:1, ...} blueprint. Mutually exclusive with example. |
example | string | A shipped example file, e.g. "hello-forge.json". |
inputs | object | Optional { name: [values] } queued into matching inbox buildings. |
ticks | number | Optional tick cap. Default 300, max 2000. |
seed | number | Optional RNG seed for reproducible runs. |
provider | object | Optional LLM provider: { kind: 'mock' | 'openai' | 'anthropic', baseUrl?, apiKey?, model? }. Default mock. Non-mock kinds are built via makeProvider from src/engine/providers.js. |
On success โ 200 with the runBlueprint result (events trimmed to
the last 50):
{ "ok": true, "outputs": {...}, "text": {...}, "ticks": 38,
"deliveries": [{ "tick": 19, "name": "out", "packet": {...} }, ...],
"events": [ ...last 50... ] }
On an invalid blueprint, unknown example, or malformed JSON โ 400 with
{ "ok": false, "error": "..." }.
Full run with an inline blueprint:
curl -s http://localhost:8123/api/run \
-X POST -H 'content-type: application/json' \
-d '{
"blueprint": {
"v": 1, "name": "shout-machine", "w": 12, "h": 6,
"entities": [
{ "t": "inbox", "x": 1, "y": 2, "r": 1, "c": { "name": "in" } },
{ "t": "belt", "x": 2, "y": 2, "r": 1 },
{ "t": "llm", "x": 3, "y": 2, "r": 1, "c": { "prompt": "Uppercase this: {{input}}" } },
{ "t": "belt", "x": 5, "y": 2, "r": 1 },
{ "t": "delivery", "x": 6, "y": 2, "r": 1, "c": { "name": "out" } }
]
},
"inputs": { "in": ["hello world"] },
"ticks": 300,
"seed": 42
}'
Run a shipped example with your own inputs:
curl -s http://localhost:8123/api/run \
-X POST -H 'content-type: application/json' \
-d '{ "example": "hello-forge.json",
"inputs": { "in": ["good morning, friend"] },
"ticks": 400 }'
Run against a real model by adding a provider (see the security notes before doing this anywhere public):
...
"provider": { "kind": "openai", "apiKey": "sk-...", "model": "gpt-4.1-mini" }
...
05 ยท CLI
server/cli.js
Run factories and inspect the catalog from a terminal:
# run a blueprint / example file, print the outputs
node server/cli.js run examples/hello-forge.json --ticks 300
# list every registered building
node server/cli.js buildings
06 ยท Providers
Bring your own key
src/engine/providers.js exports makeProvider(cfg); every provider
implements the same complete(req) interface as MockLLM, tool calls included. Plain
fetch, no SDKs:
| Config | Talks to |
|---|---|
{ kind: 'mock' } | The offline MockLLM. No network, no key. |
{ kind: 'openai', baseUrl?, apiKey, model } | Any OpenAI-compatible /chat/completions endpoint. baseUrl defaults to https://api.openai.com/v1 โ point it at https://openrouter.ai/api/v1 for OpenRouter, or your local Ollama, etc. |
{ kind: 'anthropic', apiKey, model, baseUrl? } | Anthropic's /v1/messages API. baseUrl defaults to https://api.anthropic.com. |
import { makeProvider } from './src/engine/providers.js';
import { runBlueprint } from './src/engine/api.js';
const llm = makeProvider({
kind: 'openai',
baseUrl: 'https://openrouter.ai/api/v1', // optional; default api.openai.com/v1
apiKey: process.env.OPENROUTER_API_KEY,
model: 'moonshotai/kimi-k2-thinking',
});
const result = await runBlueprint(bp, { inputs: { in: ['bonjour'] }, llm });
Where keys live
- In the browser, your key is stored in
localStorageand the page calls the provider directly โ there is no TokenForge server in the middle, no telemetry, no account. The Anthropic provider sends theanthropic-dangerous-direct-browser-access: trueheader, which is what Anthropic requires for direct browser calls; the name is a reminder that a key embedded in a page you share is a key you've given away. - Server-side (
POST /api/run), the key travels in the request body'sproviderfield and exists only for that run โ the server stores nothing.
/api/run endpoint can pass their own inputs โ and anyone who can read
your request body or page source can take the key with them. Keep keyed servers on
localhost, use per-project keys with hard spending caps, and leave public
deployments on the default mock provider.07 ยท Building reference
All 43 buildings
Generated from the engine's registered definitions (src/engine/buildings/).
Port sides are declared for facing East (rot=1) โ see the rotation
table. "Key config" lists each building's default-config keys; full schemas via
GET /api/buildings. Tier is the campaign unlock tier
(0 = available from level 1).
Core (7)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| โก๏ธ | belt | Belt1ร1 | Carries one packet per tile. Packets move one tile per tick when the next tile is free. | — | — | 0 |
| โฒ | source | Text Well1ร1 | Emits a configured text packet on a timer. | out: out:E | text, every, limit | 0 |
| ๐ฅ | inbox | API Inbox1ร1 | Emits packets pushed from outside (headless API / HTTP server / tests). | out: out:E | name | 3 |
| ๐ฆ | delivery | Delivery Depot1ร1 | Accepts packets and records them as factory output. Levels check goals here. | in: in:W | name | 0 |
| ๐ฅ | trash | Incinerator1ร1 | Destroys packets. Every factory needs a place for its mistakes. | in: in:W, in2:N, in3:S | — | 0 |
| ๐ชง | display | Signboard1ร1 | Shows the last packet that passed through, then forwards it. | in: in:W / out: out:E | — | 0 |
| ๐๏ธ | note | Note1ร1 | A sticky note on the factory floor. Purely decorative. | — | text | 0 |
Text (10)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| ๐ | case | Case Forge1ร1 | Hammers text into UPPER, lower, or Title Case. | in: in:W / out: out:E | mode | 1 |
| โ๏ธ | trim | Trimmer1ร1 | Shaves whitespace off both ends; optionally collapses inner runs to single spaces. | in: in:W / out: out:E | collapse | 1 |
| ๐ | stamp | Stamper1ร1 | Presses a prefix and suffix around every passing payload. | in: in:W / out: out:E | prefix, suffix | 1 |
| ๐ | reverse | Reverser1ร1 | Flips text end over end, character by character. | in: in:W / out: out:E | — | 1 |
| ๐ช | split | Splitter Saw1ร1 | Saws text apart on a delimiter and feeds the pieces out one per tick. | in: in:W / out: out:E | delimiter | 1 |
| ๐ | lines | Line Saw1ร1 | Saws text into its lines and feeds them out one per tick. | in: in:W / out: out:E | — | 1 |
| ๐งด | join | Gluer1ร1 | Collects incoming packets and glues them into one, on count or after a quiet spell. | in: in:W / out: out:E | count, delimiter, flushAfter | 2 |
| ๐งฉ | template | Templater1ร1 | Fills {{a}}, {{b}}, {{c}} slots from three in-ports, then emits the assembled text. | in: a:W, b:N, c:S / out: out:E | template | 2 |
| ๐ | regex | Regex Lathe1ร1 | Turns text on a regex: replace it, extract a match, or test-route pass/fail. | in: in:W / out: out:E, fail:S | mode, pattern, flags, replacement | 2 |
| ๐ข | count | Counter1ร1 | Tallies words, characters, or lines and emits the number (source text kept in meta). | in: in:W / out: out:E | unit | 1 |
Flow (9)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| ๐ช | copier | Copier1ร1 | Duplicates each packet: one copy out the front, one out the side. | in: in:W / out: out:E, copy:S | — | 1 |
| โ๏ธ | balancer | Balancer1ร1 | Splits a stream round-robin across two outputs; spills to the free side when one backs up. | in: in:W / out: a:E, b:S | — | 2 |
| ๐ซ | merger | Funnel1ร1 | Merges two streams into one, alternating fairly between inputs. | in: a:W, b:N / out: out:E | — | 1 |
| ๐ฆ | gate | Gate1ร1 | Passes packets while open. Control packets on the top port open (truthy) or close (falsy) it. | in: in:W, ctrl:N / out: out:E | invert | 2 |
| ๐ค๏ธ | router | Switch Yard1ร1 | Routes packets that match a rule out the front; everything else out the side. | in: in:W / out: match:E, else:S | mode, value | 2 |
| ๐ | delay | Slow Roller1ร1 | Holds each packet for N ticks before rolling it onward. Holds up to 4 at once. | in: in:W / out: out:E | ticks | 1 |
| ๐ฌ | buffer | Warehouse1ร1 | FIFO storage: absorbs bursts up to its size, drains one packet per tick. | in: in:W / out: out:E | size | 1 |
| ๐ฅ | sampler | Sampler1ร1 | Passes every Nth packet; the rest are quietly incinerated (tallied in state.dropped). | in: in:W / out: out:E | n | 2 |
| ๐ฐ | throttle | Valve1ร1 | Lets at most one packet through every N ticks. The rest queue up behind it. | in: in:W / out: out:E | ticks | 2 |
Data (9)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| โ๏ธ | jsonparse | JSON Smelter1ร1 | Smelts raw text into a json packet via JSON.parse. Slag (unparseable text) exits the error chute. | in: in:W / out: out:E, err:S | — | 2 |
| โ๏ธ | jsonget | Extractor1ร1 | Mines a value out of a json packet by path, e.g. a.b[0].c. Misses fall back or get dropped. | in: in:W / out: out:E | path, fallback | 2 |
| ๐๏ธ | jsonbuild | Assembler1ร1 | Stamps each packet into a JSON template โ every {{input}} in the template becomes the packet text. | in: in:W / out: out:E | template | 3 |
| ๐พ | csv | CSV Thresher1ร1 | Threshes CSV text into one json packet per row โ objects when the first row is a header. | in: in:W / out: out:E | header | 2 |
| ๐งฎ | math | Math Mill1ร1 | Grinds packets through an arithmetic expression. x = numeric payload, len = text length. | in: in:W / out: out:E | expr | 2 |
| โ๏ธ | compare | Comparator1ร1 | Weighs each packet against a value; winners exit east, losers drop south. | in: in:W / out: pass:E, fail:S | op, value | 2 |
| ๐ข๏ธ | accumulate | Silo1ร1 | Hoards packets until the count is reached, then dumps one combined packet (collect / sum / concat). | in: in:W / out: out:E | mode, count, delimiter | 3 |
| ๐ฏ | dedupe | Deduper1ร1 | Lets each payload through once; repeats go to the scrap heap. Limited memory, oldest forgotten first. | in: in:W / out: out:E | memory | 2 |
| ๐ | tally | Tally Board1ร1 | Chalks up how often each payload has come through; emits {key, count} for every packet. | in: in:W / out: out:E | — | 3 |
LLM (4)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| ๐ฎ | llm | Prompt Forge2ร2 | Runs each packet through an LLM prompt template and emits the reply. | in: in:W / out: out:E | system, prompt, model, temperature | 2 |
| ๐ง | classify | Sorting Sage1ร1 | Asks an LLM to classify each packet and routes it out the matching port. | in: in:W / out: a:E, b:S, c:N | labels, model | 3 |
| โ๏ธ | judge | Judge1ร1 | Scores each packet against a rubric and emits the number. | in: in:W / out: out:E | rubric, scale, model | 3 |
| โ๏ธ | distill | Distiller1ร1 | Boils text down to structured JSON via an LLM. Unparseable replies pass through as text. | in: in:W / out: out:E | instructions, model | 3 |
Script (1)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| ๐ | script | Script Engine1ร1 | Runs your JavaScript on every packet. `return` sends out the front; emit(x, "alt") drops out the side hatch. `state` persists between packets. | in: in:W / out: out:E, alt:S | code, timeoutMs | 3 |
Agent (3)
| id | Building | Description | Ports | Key config | Tier | |
|---|---|---|---|---|---|---|
| ๐ค | agent | Agent Core2ร2 | Runs a tool-calling loop on task packets. Docks with adjacent Toolboxes, Memory Shelves, and other Agent Cores (sub-agents). | in: task:W / out: result:E | name, system, maxSteps, model | 4 |
| ๐งฐ | tool | Toolbox1ร1 | A scripted tool for adjacent Agent Cores. Inside the code, `args` holds the parsed tool arguments; return the result. | — | name, description, params, code | 4 |
| ๐๏ธ | memory | Memory Shelf1ร1 | Labeled jars for agent memories. Adjacent Agent Cores can set, get, and list values that persist across tasks. | — | name | 4 |
08 ยท Agents & tools
Agents are built out of bricks
The Agent Core (agent, 2ร2) runs a tool-calling loop on each
packet arriving at its task port and emits the final answer from
result. What makes it a factory game: its tools are discovered by physical
adjacency. When a task arrives, the agent scans every building touching its footprint
(sim.grid.neighborsOf(ent)) and collects:
- Any building whose definition has a
toolproperty โ in the shipped set that's the Toolbox (tool) and the Memory Shelf (memory), but any building def can opt in. - Adjacent Agent Cores, exposed as delegable sub-agent tools named
agent_<name>with a singletaskstring parameter. Calling one runs that agent's own loop (with its adjacent tools) and returns its final text. Delegation depth is capped at 3.
The tool def contract
A building becomes agent-callable by declaring tool(ent, sim) on its definition,
returning:
{
name: 'shout', // tool name the model sees
description: 'shout the input', // the model reads this to pick tools
parameters: { type: 'object', properties: { input: { type: 'string' } } }, // JSON Schema
run: async (args) => 'result string',
}
The Toolbox fills this contract from its config: name,
description, params (JSON Schema, as an object or a JSON string), and
code โ a sandboxed JS body where args holds the parsed tool arguments
and the return value becomes the tool result (1000 ms timeout). The
Memory Shelf exposes one tool with
op: 'set' | 'get' | 'list' plus key/value; its jar
(ent.state.kv) persists across tasks.
The loop, maxSteps, and the trace
Per task the agent builds messages = [{role:'user', content: task}] and repeats,
up to config.maxSteps times (default 6, clamped to โฅ1):
- Call
api.llm({ system, messages, input, tools })โ tools included only when any were discovered;config.modeloverrides the session model when set. - If the reply has
toolCalls, run each one (unknown names and thrown errors becomeERROR: ...results), append the assistant turn and onerole:'tool'message per call, and loop. - Otherwise the reply text is the final answer โ emitted from
resultas a text packet withmeta.agent(the agent's name) andmeta.steps.
If the step cap is hit without a final answer, the agent falls back to its last assistant
text, or [agent] step limit reached. Every step is recorded in
ent.state.trace โ entries of { step, action: 'call' | 'tool' | 'final',
detail }, capped at the last 50 โ which the game UI displays live.
Complete example: agent + toolbox
The Agent Core occupies (2,2)โ(3,3); the Toolbox at (2,4) touches its bottom edge, so it's discovered automatically โ no wiring needed:
const agentBp = { v: 1, name: 'agent-demo', w: 14, h: 8, entities: [
{ t: 'inbox', x: 1, y: 2, r: 1, c: { name: 'task' } },
{ t: 'agent', x: 2, y: 2, r: 1, c: { name: 'helper' } },
{ t: 'tool', x: 2, y: 4, r: 1, c: {
name: 'shout', description: 'shout the input text loudly',
params: { type: 'object', properties: { input: { type: 'string' } } },
code: 'return String(args.input).toUpperCase() + "!!!"' } },
{ t: 'delivery', x: 4, y: 2, r: 1, c: { name: 'out' } },
]};
const r = await runBlueprint(agentBp, { inputs: { task: ['please shout: hello'] } });
console.log(r.outputs.out); // [ 'Result: PLEASE SHOUT: HELLO!!!' ]
Under MockLLM, the tool heuristic picks shout by word
overlap, then summarizes the tool result โ deterministically. Swap in a
real provider and the very same factory runs a real tool-calling
agent.