A technical note for developers who want to build simulations where information—not resource management—is the mechanic.
The design constraint
The central constraint was conceptual, not visual: reality must not move. The interesting system is every version of that reality that people form and transmit.
So the app begins with one protected record:
const event = 'A dragon attacked the eastern farm at dawn.';
The map is then a readout of thirty separate interpretations. A farmer can speak from proximity, a scholar can hesitate, and a child can leap to a dramatic conclusion. The user sees the social network rather than being asked to micromanage it.
The browser architecture
React owns the exhibit state. The provider adapter is deliberately separate from the rumour orchestrator: the adapter knows how to call an endpoint; the orchestrator knows the information rules. That boundary lets the same citizen workflow run against a hosted model, a local model, or an Agno agent.
A provider-neutral model adapter
Most model servers now implement OpenAI-style chat completions. One small function can therefore cover OpenAI, gateways, Ollama’s compatibility server, and LM Studio:
const response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({
model,
temperature: 0.7,
response_format: { type: 'json_object' },
messages,
}),
});
The Agno citizen service takes a different route. It accepts a run for one named citizen, so the orchestration and role rules remain inside real Agno agents:
await fetch(`${agentOsUrl}/agents/${agentId}/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ message: prompt, stream: false }),
});
The workflow: model output as a constrained simulation input
The prompt mirrors a small agent workflow: observe the permanent record, retrieve current memory, reason through a role, converse, then reflect. The response is constrained to a compact JSON payload.
const value = JSON.parse(match[0]) as BeliefUpdate;
if (!value.belief || !Number.isFinite(value.confidence)) {
throw new Error('The model response was missing a belief or confidence.');
}
return { ...value, confidence: Math.max(0, Math.min(100, Math.round(value.confidence))) };
This detail matters. A model is not authoritative simply because it returns prose. The simulation treats every model answer as untrusted input, parses it, clamps its numeric ranges, and only then adds it to a citizen state.
Why the map uses CSS and SVG
The brief calls for a miniature village, but a full 3D scene is not automatically the better first implementation. The map’s job is to show relationships, distance, and transmission. CSS buildings and an SVG network do that at a tiny payload and make the flow lines easy to inspect.
The visual direction is closer to a field notebook than an AI dashboard: muted terrain, editorial type, old-paper profile cards, and one warm highlight for the active thread. The interface’s signature is the glowing rumor line—the visual equivalent of a sentence moving from person to person.
The current model consultation is intentionally individual and opt-in. A deeper version should add first-class rumours, a maximum memory queue, trust deltas, deterministic scheduled encounters, and replayable seeds. Once those pieces exist, model calls can be used selectively—for example only when a novel rumour reaches a high-influence citizen—while the majority of the simulation remains cheap, traceable, and reproducible.
For a public deployment, move the model calls behind a server-side proxy. A browser should never be the long-term home of a paid provider credential.
Code & more: https://www.dailybuild.xyz/project/213-the-rumor-machine

Top comments (0)