Skip to main content
Reference for your agent. The precise shape of things — signatures, fields, rules — written so an agent can read it and act. You do not need to: ask your agent for what you want in words, and if it needs this page, hand it the URL or use Copy page. What to ask for, and how to check it, is in Ask; how to see what changed is in Look inside.
Four working programs. Each one is a directory under programs/ in your harness, and each is here because the shape is what is hard, not the code.

A router: different agents for different messages

handle(msg) is a function rather than a setting because who should answer is the one thing only your harness can know. Read msg.text for a command, msg.user_id for who is asking, msg.chat_id for where.
What that answers, message by message:
Three things worth noticing. The Support agent has no shell — system:search narrowed to two tools is the whole of what it can do, so a customer question cannot turn into a command. Engineer can spawn the Executor and Support cannot, because agents= is per agent. And the /support branch hands back a rewritten message, which is what that agent is given; the chat still holds what the person actually typed.
All three run on the same programs/splox/hooks/, because hooks belong to the program. If Support needs a guard Engineer does not, the guard reads t.agent.

A bot that answers from outside a chat

There is no connections page for this. A Telegram bot, a webhook, a mailbox watcher — each is a program in your harness, started once, running as long as the machine does.
Start it from a chat on this machine, once:
It keeps running after that turn ends, after that chat ends, and into tomorrow. It stops when the machine stops.

Secrets are environment variables

TELEGRAM_BOT_TOKEN is injected into every exec on this machine, and the program reads os.environ — nothing writes a token into a file in git. Read it inside the function that needs it: the platform imports main.py every time it needs to know who an agent is, and it imports from processes that do not carry your secrets. The trap, in full.

Restarting it is harmless

The offset is on disk and the loop reads it on start, so starting the program twice does not answer anything twice. Write every loop this way and “has it stopped?” stops being a question anybody has to answer.
Each of those support(...) calls is a real run: it appears in the chat list with its whole conversation, and it is billed like any other. So the program logs its inputs, the run records its own reasoning, and together they say what happened:

A nightly job

Same shape, different trigger. There is no scheduler to register with: the program is the scheduler, and the thing that makes it correct is the state file.
due() asks about the calendar, not about the loop:
That is the difference between a job that runs once a day and a job that runs whenever the process happened to be restarted. A machine that was asleep at 6 runs the digest the moment it wakes; a machine restarted three times at noon runs it once. The digest’s own answer goes into the state file and back into tomorrow’s prompt, which is the cheapest kind of memory there is: one string, written where the next run will look for it.

A fan-out over many items

Work that produces many results at once is a program, not a chat turn: it needs a loop, a ledger and a place to put what came back.
Four decisions in there are the pattern: The schema is the only gate. Whatever an answer must satisfy goes in schema=. A violation is shown back to the same agent and retried inside its own run, so a constraint costs one more turn instead of a lost phase. Do not hand-write a validator over ids an agent invented. A failure is a hole, not an exception. parallel puts None in the slot and the exception in .errors[i], and nothing after it shifts. The ledger says which items are missing, by name, which is the only honest thing to report when 47 of 50 pages came back. The readers write nothing. Each auditor is told to write nothing to disk, and only the final executor gets a workspace=. A reviewer that can commit into the tree it is judging is not a reviewer. Read-only is not a parameter; it is what the message says. Three at a time unless you say otherwise. Pool(8) raises the live-agent cap for that block. Without it, eight parallel units still run three at a time.

When the fan-out needs phases

An audit is one round. Work that plans, builds in parallel, integrates and then verifies until the tests pass is the same idea with a make-review-repair cycle around it, and that shape is worked out in full in your own checkout:
Read the README before writing anything of that shape. ask() is one typed task, map() is parallel work with one worktree each, and a checked ask() is the same call with until= and repair= around it:
orchestrator.py sits in the program rather than on the platform on purpose: how a job is cut into phases, what invalidates a checkpoint, when a repair loop is declared stuck are decisions a program should be able to disagree with. Copy the directory and rewrite the file when your work wants a different shape.
None of this is for a question, an explanation or one local edit. Call the agent once and be done.