community-bot

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

  1. Receive. The Gateway Durable Object posts a Discord event to /webhook. The Worker checks x-discord-gateway-token before reading the body.
  2. De-duplicate. DedupeDO.seen(message.id) checks a seven-day ledger. A replayed event stops here and returns duplicate.
  3. Log. Every non-bot message is appended to messages, whether or not the bot was mentioned. Channel history is the base layer for context.
  4. Handle commands. !memory ... and @bot memory ... are parsed before anything else. Commands work without a mention and return without generating a reply.
  5. 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.
  6. Stop if not mentioned. The turn ends after logging when the bot was not addressed. Reactions and corrections are the only side effects.
  7. Load context. The last 20 messages in the channel, plus the active style override, are read from ConversationDO.
  8. 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.
  9. 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.
  10. 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.
  11. Remember. After the reply is posted, extractMemories runs 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:

  1. An optional channel summary, when one exists.
  2. An optional system message with up to ten memories about the current speaker.
  3. The recent conversation, oldest to newest. Human messages are prefixed with their display name; bot messages become assistant turns.
  4. 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. TaskAgent is 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_search and read_page go through Jina (30-second timeout, 8,000 characters per result; read_page accepts HTTP(S) only). run_command runs bash -lc in the channel's sandbox container with a 60-second timeout, keeping the last 4,000 characters of output.
  • Workspace. /workspace keeps 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, runTask returns null and 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.