Cloudflare Workers + Durable Objects runtime for small networks of LLM-backed bots.
rede is a Cloudflare Workers project for running a small network of LLM-backed bots on top of Durable Objects.
Each bot in bots.json maps to a named Durable Object instance. Bots keep their own message history and coordination ledger, exchange messages through worker endpoints, and optionally use a constrained web_fetch tool to inspect public web pages.
- One Durable Object class, many named bot instances.
- Message-driven bot coordination with persistent per-bot state.
- Short-lived sessions with cooldowns, reply budgets, and rate-limit backoff.
- Structured task updates (
claim,request,report,complete) attached to otherwise natural chat messages. - Lightweight observability through an event stream and HTML timeline.
This is not a general workflow engine, job queue, or arbitrary graph runtime for long-lived autonomous agents.
src/index.ts: worker entrypoint and HTTP API.src/bot.ts: Durable Object runtime for each bot.src/logger.ts: event logging and durable event log storage.bots.json: bot definitions.game.json: optional scenario-mode configuration.examples/story-circle/: reusable story-circle bot and scenario preset.examples/chess-coordinates/: experimental chess-turn preset withCacting as board state.scripts/deploy-bots.mjs: deploy selected bots and verify healthchecks.scripts/kill-bots.mjs: destroy and recreate the bot Durable Object namespace.scripts/archive-timeline.mjs: save the event stream and HTML timeline for a run.
Bots are declared statically in bots.json:
[
{
"name": "A",
"prompt": "Research role: editor-synthesizer...",
"createdAt": "2024-01-01T00:00:00.000Z",
"speed": 2
}
]Supported fields:
name: bot identifier and Durable Object instance name.prompt: instructions for the bot.createdAt: optional seed timestamp.isCoordinator: optional explicit coordinator flag for presets that need a specific routing hub.speed: optional delay, in seconds, before the bot responds.llmApiKey: optional per-bot API key override.
All bots use the same Durable Object class: BotDurableObject.
The recommended deployment flow is:
- Deploy the worker with
BOT_DEPLOY_TARGETSset to the active bot names. - Call each bot's
/deployendpoint. - Verify
/healthfor each selected bot.
Explicit deploys seed state and announce presence to peers with an "I'm here" message. Ordinary API reads, event polling, and internal bot-to-bot messaging do not auto-redeploy the swarm.
Messages can enter the system through:
POST /bots/:name/messagefrom a client.- Presence announcements during explicit deploys.
- Follow-on messages emitted by another bot.
When a bot receives a non-presence message, it:
- appends it to persistent history
- starts or continues a session window
- waits for its configured delay
- builds a prompt from instructions, recent conversation, peers, and coordination state
- optionally calls
web_fetch - emits a JSON-formatted reply containing a natural-language message plus optional coordination updates
- stores the reply locally and dispatches it to selected peers
Per-bot brain execution is serialized. If a newer substantive message lands while an older turn is still running, the older reply is dropped as stale before dispatch.
Bots can attach structured task updates:
{
"message": "short update",
"recipients": ["B"],
"coordination": [
{
"type": "claim",
"taskId": "collect-primary-source-links",
"owner": "B",
"summary": "Gather direct source URLs for each cited stat"
}
]
}Supported coordination.type values:
claimrequestreportcomplete
Runtime handling:
claimmarks a taskin_progressrequestmarks a taskblockedreportupdates the task summary without forcing completioncompletemarks a taskdone
If a sender does not include structured coordination, the runtime can still infer simple task claims from plain text such as "I'll check the docs" or "I'm going to verify that stat".
Bots do not run indefinitely. By default, a session lasts 120 seconds from the first non-presence message in that session.
The runtime also enforces:
- a minimum cooldown between local LLM replies
- a maximum number of replies per session
- temporary backoff after upstream rate-limit errors
- duplicate outbound suppression for recent messages
The worker exposes a recent event stream as NDJSON and an HTML timeline. Events are persisted in a dedicated Durable Object (EventLogDurableObject) with a bounded buffer intended for development and inspection, not durable production analytics.
game.json enables an optional scenario/evaluation mode. In the default repository state, it is configured for a distributed-research demo and logs a structured outcome event when a run ends.
If you do not want this behavior, disable or replace the contents of game.json.
The repository includes reusable presets under examples/.
examples/story-circle/bots.json: three bots that pass one story around in a circle, one sentence at a time.examples/story-circle/game.json: optional scenario-mode settings for evaluating whether the circle produced a coherent story.examples/chess-coordinates/bots.json:Aplays White,Bplays Black, andCacts as the board-state keeper using chess notation only.examples/chess-coordinates/game.json: optional scenario-mode settings for evaluating turn order and notation discipline in the chess demo.
The active root bots.json and game.json currently match that story-circle configuration.
- Node.js 18+
- a Cloudflare account
- Wrangler
- an OpenAI-compatible API key
npm installCreate .dev.vars from the checked-in example:
cp .dev.vars.example .dev.varsThen edit .dev.vars and add your API key.
Start the worker locally:
npm run devDefault local URL:
http://localhost:8787
If you want local Durable Object persistence:
npm run deploy:localWorker/runtime variables:
OPENAI_API_KEY: shared model API key.BOT_DEPLOY_TARGETS: JSON array of active bot names. Defaults to[]in Wrangler config.DISABLE_AUTO_DEPLOY: set totrueto disable first-read bot initialization.DISABLE_AUTO_ANNOUNCE: set totrueto suppress"I'm here"fanout during explicit deploys.SESSION_KILL_AFTER_SECONDS: session duration. Default120.BRAIN_COOLDOWN_SECONDS: minimum delay between local LLM replies. Default4.MAX_SESSION_REPLIES: maximum LLM replies per session. Default6.RESERVED_FINAL_REPLIES: replies reserved for late-session coordination. Default1.RATE_LIMIT_BACKOFF_SECONDS: cooldown after upstream rate-limit pressure. Default15.RUN_ID: optional label used by archive tooling and scenario evaluation.
Script-only variables:
BOT_HEALTHCHECK_URL: base URL used by helper scripts when--urlis omitted.
List active bot definitions with API keys redacted.
Deploy or refresh one bot's stored state.
Effects:
- persists bot metadata
- stores the current peer roster
- seeds initial history if needed
- announces presence to peer bots
Return one bot profile. If the bot has not been initialized yet, the worker initializes it on demand.
Run a healthcheck against the bot Durable Object.
Send a message from one bot to another:
{
"to": "B",
"content": "Start with the pricing pages."
}The sender bot is the :name path segment.
Clear one bot's Durable Object state.
Clear all active bot state and reset the event log.
Return the recent event stream as NDJSON.
Return a simple HTML timeline for the recent event stream.
Clear the event log.
Deploy all configured bots:
npm run deploy:bots -- --url https://<your-worker-subdomain>Deploy a subset:
npm run deploy:bots -- --bot A --bot B --url https://<your-worker-subdomain>What deploy:bots does:
- validates requested bot names
- deploys the worker with
BOT_DEPLOY_TARGETSset to the selected list - calls each bot's
/deployendpoint - polls
/healthuntil each selected bot responds
Archive the current event stream and timeline:
npm run archive:timeline -- --run my-run-id --url http://localhost:8787Watch for conversation stop events and archive automatically:
npm run archive:auto -- --run my-run-id --url http://localhost:8787Run local dev and auto-archive together:
npm run dev:archive -- --run my-run-id --url http://localhost:8787Generated artifacts are written under runs/. The repository now ignores that directory by default.
To delete all bot Durable Objects and recreate a fresh namespace:
npm run kill:botsThis is destructive.
For local testing, a lighter reset is usually enough:
curl -X POST http://127.0.0.1:8787/bots/reset
curl -X POST http://127.0.0.1:8787/bots/events/resetBefore publishing a public fork, review:
- prompts in
bots.json - scenario configuration in
game.json - Cloudflare account bindings and deployment names in
wrangler.jsonc
Do not commit real API keys, private deployment URLs, or archived production runs.
- single worker, single Durable Object class for bots
- peer-to-peer messaging goes through HTTP endpoints, not direct Durable Object RPC
- no durable queue, scheduler, or retry system for unfinished work
- event logs are for inspection, not production-grade observability
- prompts still do a large share of coordination work