Town of agents: 50 characters, one burst
1. What Jev decides
For each of 50 characters, what it does when an event is announced: ignore, investigate, join, flee, or warn. A second question scores urgency on five levels to set walking speed. All 50 calls fire at once with Promise.all. The state per character is its name, role, traits, energy, location, the event text, and the event's location. The characters and ten places were written once by anthropic/claude-sonnet-5 into town.json and are committed, so every run sees the same town.
2. The questions
The questions and the generation prompt, from the top of town.mjs. This is the block a reviewer must read; the rest of the file is plumbing.
// ---- Review these. Everything else is plumbing. ----
const QUESTIONS = {
action: {
type: "choice",
instructions: "The character has just heard the event. What does this specific character, with these traits and this role, do next?",
criteria: {
ignore: "Carries on with the current task; the event does not concern this character",
investigate: "Goes to see what is happening",
join: "Takes part in it, or helps the people involved",
flee: "Gets away from it to somewhere safe",
warn: "Goes to tell other people about it",
},
},
urgency: {
type: "score",
instructions: "How urgently does the character act on the event?",
criteria: ["none: no change of pace", "low: when convenient", "medium: soon", "high: right away", "extreme: drops everything and runs"],
},
};
const GEN_MODEL = "anthropic/claude-sonnet-5";
const GEN_PROMPT = `Invent a small medieval town for a simulation. Output only JSON, no prose, no code fence, with this shape:
{"places":[{"name":"bakery","x":20,"y":30}, ...], "characters":[{"name":"Mara","role":"baker","traits":"cautious, generous","place":"bakery","energy":0.8}, ...]}
Rules: 10 places (include bakery, mill, gate, well, market, tavern, forge, farm, chapel, river) with x and y from 5 to 95.
Exactly 50 characters with distinct names and varied roles (include exactly one baker, one miller, one gatekeeper, one priest, one blacksmith, a few children, a few elders, farmers, traders, guards, a thief, a healer).
Traits: two or three adjectives that differ a lot between characters (brave, cowardly, nosy, lazy, greedy, kind, suspicious, ...). place: one of the 10 place names. energy: 0.1 to 1.0.`;
3. What the page shows
A top-down map with the ten places as dark circles and each character as a coloured dot with its name. You type an event and pick a place. The dots go grey while the burst runs, then take the colour of their action: teal investigate, orange join, red flee, yellow warn, grey ignore. Each dot carries a small bar for the probability of its action. Dots walk toward or away from the event at a speed set by urgency. The side panel shows answered/total, burst wall time, decisions per second, median and max per call, cost, the action counts, and a list of every character sorted by urgency.
4. Run it
node --env-file=.env town/town.mjs # http://localhost:3002
node --env-file=.env town/town.mjs --batch 10 # 10 calls per batch instead of one burst of 50
node --env-file=.env town/town.mjs --check
node --env-file=.env town/town.mjs --gen # rewrite town.json (not needed; it is committed)
5. The check, verbatim
"Free bread at the bakery": 50/50 ok, burst 6876 ms, 7.3/s, median 515 ms, max 6873 ms, $0.0011, actions {"ignore":4,"investigate":36,"join":7,"flee":0,"warn":3}
"A wolf is at the gate": 50/50 ok, burst 9991 ms, 5/s, median 420 ms, max 9990 ms, $0.0011, actions {"ignore":0,"investigate":29,"join":0,"flee":3,"warn":18}
flee+warn: bread 3, wolf 21; join: bread 7, wolf 0; baker on bread → join (p 0.69, urgency 1.1)
Two earlier runs of the same two events, before the check was rewritten:
"Free bread at the bakery": 50/50 ok, burst 6857 ms, 7.3/s, median 512 ms, max 6853 ms, $0.0011, actions {"ignore":1,"investigate":41,"join":7,"flee":0,"warn":1}
"A wolf is at the gate": 50/50 ok, burst 2655 ms, 18.8/s, median 432 ms, max 2655 ms, $0.0011, actions {"ignore":0,"investigate":34,"join":0,"flee":3,"warn":13}
"Free bread at the bakery": 50/50 ok, burst 2775 ms, 18/s, median 643 ms, max 2771 ms, $0.0011, actions {"ignore":3,"investigate":36,"join":8,"flee":0,"warn":3}
"A wolf is at the gate": 50/50 ok, burst 6622 ms, 7.6/s, median 376 ms, max 6622 ms, $0.0011, actions {"ignore":0,"investigate":30,"join":0,"flee":3,"warn":17}
6. Findings
- All 50 calls returned in all six bursts. The burst wall time ranged from 2.7 to 10.0 s while the median per call stayed at 376 to 643 ms. One slow call sets the burst time. Decisions per second on screen ranged from 5 to 18.8 for the same work.
- The assessment's proof check said the modal action must differ between the two events. It does not: investigate won both, 36 and 29 of 50 in the final run, 41 and 34 in the first. The check was rewritten to what does differ. The wolf draws flee and warn (21 against 3); the bread draws join (7 against 0). The comment in
town.mjsrecords this. - One rewording of the instruction, adding "this specific character, with these traits and this role", moved the counts by a few characters and did not change the modal action.
- The baker joined the free bread at p 0.69 in the final run and p 0.75 and 0.68 in the earlier ones. The spec's second assertion, that the baker does not ignore it, held every time.
- Characters with the same traits do answer alike. The generation prompt asks for traits that differ a lot for that reason.
7. Cost and latency
One event: 50 calls with two questions each, $0.0011. An hour of continuous events at 5 calls per second would be about $0.50.