Agent workflow
A turn is a short pipeline with one hard rule: decide first, generate second, remember after. This page follows a message from the gateway to the reply, including the branches that skip generation entirely.
The turn
- Receive. The Gateway Durable Object posts a Discord event to
/webhook. The Worker checksx-discord-gateway-tokenbefore reading the body. - De-duplicate.
DedupeDO.seen(message.id)checks a seven-day ledger. A replayed event stops here and returnsduplicate. - Log. Every non-bot message is appended to
messages, whether or not the bot was mentioned. Channel history is the base layer for context. - Handle commands.
!memory ...and@bot memory ...are parsed before anything else. Commands work without a mention and return without generating a reply. - Detect corrections. If the message is a reply to a bot message, Jev is asked whether it corrects the bot. A correction is recorded as a signal even when the bot is not mentioned.
- Stop if not mentioned. The turn ends after logging when the bot was not addressed. Reactions and corrections are the only side effects.
- Load context. The last 20 messages in the channel, plus the active style override, are read from
ConversationDO. - React immediately. Two Jev questions run in parallel: is a single emoji enough (a boolean), and which emoji fits (a choice). The reaction is added first, as the immediate acknowledgment. If the emoji alone is enough, the turn ends here.
- Decide whether this is a task. Jev answers a second boolean: does this need multi-step work outside the chat? If yes, the turn runs on the task agent.
- Produce the response. The task agent may return text; if it fails or returns nothing, the turn falls through to the written reply. The text path loads the speaker's memories and calls the chat model.
- Remember. After the reply is posted,
extractMemoriesruns in the background. Its failure cannot affect the reply.
The webhook itself returns ok before steps 3โ11 complete. ctx.waitUntil keeps the work alive after the response.
The decision boundary
The split between Jev and the language model is the most important structural choice in the system. Jev answers closed questions; the language model and the task agent produce open text.
| Question | Answer type | Caller decides |
|---|---|---|
| Is a single emoji enough? | noul |
Reaction only when probability is at least 0.5; otherwise continue to the written reply |
| Which emoji fits? | choice |
One of ๐ ๐ ๐ค ๐; fallback ๐ |
| Is this a complex task? | noul |
Run the task agent when probability is at least 0.5; otherwise use the written reply |
| Is this a correction? | noul |
Record when probability is at least 0.5 |
| Is this exchange worth remembering? | noul |
Extract when probability is at least 0.5 |
| Keep, update, or delete this memory? | choice |
Apply only the verdicts Jev returned |
| Should a new memory be added? | noul |
Add when probability is at least 0.5 |
| Keep or delete a stale memory? | choice |
Delete only on delete; a failed call keeps it |
| Did the reply stay in character? | choice |
kept, sermon, or shift |
If Jev cannot be reached, the default is always the conservative branch: react with ๐, continue to the written reply, no correction, no memory write. The reaction itself is best-effort; a failed reaction is logged and never blocks the rest of the turn.
Response shapes
| Shape | What happens |
|---|---|
| Reaction only | The bot adds one emoji to the triggering message and stops. No text is posted. |
| Written reply | The default. Builds the prompt, calls the chat model, and posts a reply. |
| Task | Jev says the request needs multi-step work, so the channel's task agent runs with tools. Its answer is posted as one message. |
The reaction is sent before the task decision in every case, so a task that takes twenty seconds still gets an immediate acknowledgment. A failed reaction is only a log line.
Prompt assembly
The text prompt is assembled in src/prompt.ts and has a fixed order:
- An optional channel summary, when one exists.
- An optional system message with up to ten memories about the current speaker.
- The recent conversation, oldest to newest. Human messages are prefixed with their display name; bot messages become
assistantturns. - The persona block, as the final system message.
The persona goes last on every turn. It is rebuilt each time from persona.md, with the active style override if the nightly revision has applied one. The core section is never replaced.
The task path builds a different, single user message: the last ten lines of the channel and a sentence naming the request and the member who asked. The agent adds the persona itself on every turn through its beforeTurn hook.
The task path
The task agent is the one place in the system with a tool loop. It is bounded on purpose.
- Session.
TaskAgentis one Cloudflare Agents SDK session per channel. It keeps its own turn history, so a follow-up task in the same channel continues the previous work. - Tools.
web_searchandread_pagego through Jina (30-second timeout, 8,000 characters per result;read_pageaccepts HTTP(S) only).run_commandrunsbash -lcin the channel's sandbox container with a 60-second timeout, keeping the last 4,000 characters of output. - Workspace.
/workspacekeeps files for as long as the container runs, and each channel has its own agent and container. The container starts on demand with internet access and stops after five minutes of inactivity. - Output. One completed turn becomes one Discord message. The agent is told not to include preamble or a work log.
- Fallback. If the turn fails, times out, or produces no text,
runTaskreturnsnulland the normal written reply runs.
The web tools are called with the bot's Jina key from the Worker; the container holds no credentials, and the model chooses the commands.
Signals after the reply
Two kinds of feedback are recorded for the nightly style revision:
- Reactions on bot messages. Only reactions added by someone other than the bot are stored, and only when the target message was authored by the bot.
- Corrections. A reply to a bot message is classified by Jev; a positive result stores the user's text as a correction signal.
Both land in the signals table with a unique constraint on (message_id, user_id, kind, value), so a replayed gateway event does not count twice.
What is not here
There are no slash commands: the Gateway does not forward INTERACTION_CREATE, so commands are plain text. The task agent is the only path with a tool loop; the conversational path is one decision, one generation, and at most one response per message.