Skip to content

AncientPatata/pebble

Repository files navigation

Pebble

Pebble is a self-hosted Discord bot with a persistent memory system and a configurable personality. It is not a general-purpose assistant. It is designed to feel like a genuine member of a server: forming impressions over time, recalling past conversations, adapting its tone to the room, and occasionally jumping in without being asked.

Built on Pydantic AI and the OpenRouter API, it runs comfortably on modest hardware with a SQLite database and a local ChromaDB vector store.


What makes it different

Most Discord bots that use LLMs treat each message in isolation. Pebble does not. Every interaction feeds into a four-tier memory system that persists across restarts:

  • Core memory is always in context: who the bot is, its current mood and energy, what it knows about the server owner, and a directory of channels it has been active in.
  • Working memory holds the last N messages per channel, formatted as a proper conversation history passed to the LLM.
  • Episodic memory stores summarized conversation segments generated by a background compaction agent. When the working buffer fills up, the oldest messages are collapsed into a narrative summary and stored with topic tags and an emotional valence score.
  • Archival memory holds discrete facts extracted during compaction: birthdays, preferences, server lore, relationship notes. Both episodes and archival facts are embedded via OpenRouter and stored in ChromaDB for semantic retrieval.

The result is a bot that can, within reason, answer "do you remember when we talked about X?" with something useful, and that gradually builds a profile of the people it talks to.


Personality and persona

The bot's personality is defined entirely in config.yml under agent_info.persona. It is a plain text description that is injected as the identity block of core memory on every load. Changing this field and saving the file hot-reloads the persona without a restart.

Two system prompt modes ship out of the box:

  • Social mode is used in server channels. The bot behaves like a friend-group member: casual, opinionated, willing to disagree or roast someone, aware of its current time of day and energy level.
  • Intimate mode is used in DMs with the configured owner. It is warmer, more emotionally attentive, and has access to the full tool set including scheduling and calendar integration.

All system prompts can be overridden by dropping markdown files into the prompts/ directory. The files are hot-reloaded when changed. This means you can iterate on the bot's voice without restarting it.


Routing

Not every message gets a full LLM response. Before the main agent runs, a lightweight classifier (Haiku by default) decides whether to respond, ignore, or jump in uninvited. It has access to recent message context and can detect soft mentions: cases where someone is clearly talking to Pebble without using an @mention.

Untagged messages go through an additional engagement decay system. The more Pebble has responded without being directly mentioned, the lower the probability it will respond again. A hard @mention resets this counter. This prevents the bot from dominating every conversation it wanders into.

The router also selects which tool categories the main agent needs, so the context window is not polluted with tool definitions for capabilities the current message does not require.


Memory tools

The agent has access to tools for reading and writing all memory tiers during a response. It can:

  • Update or append to core memory blocks
  • Store archival facts about users or the server
  • Search past episodes semantically
  • Look up user profiles and Discord IDs for @mentions
  • Read channel message history beyond the working memory window
  • Update its own mood, energy, and vibe

These calls happen transparently during the agent's tool-use loop. From a user's perspective, the bot just seems to remember things.


Background reflection

Every N messages (configurable), a background reflection pipeline runs against the current channel's working memory. It:

  1. Compacts the oldest messages into an episode summary with topic tags and fact extraction
  2. Checks whether recent responses have drifted from the persona definition, and self-corrects if so
  3. Evolves the bot's persona state (mood, energy, vibe) based on what just happened
  4. Updates the channel context directory in core memory

A separate daily deep reflection runs on a configurable schedule. It reviews all episodes from the past 24 hours, updates user profiles, refreshes the owner block, and generates a journal entry and a list of follow-ups stored as commitments.


MCP support

Pebble supports MCP (Model Context Protocol) servers configured in config.yml. Servers can be stdio-based (local processes), SSE, or HTTP. Each server is assigned a category name that the router can select, so only relevant tools are active per request. The intimate agent has access to all servers unconditionally.

This makes it straightforward to add tools like Home Assistant control, filesystem access, or any other MCP-compatible service without touching the core codebase.


Deployment

The project ships with a docker-compose.yml that runs Pebble alongside Dozzle (log viewer) and Datasette (read-only SQL browser for the database). The source directory and config file are volume-mounted, so you can edit prompts and config without rebuilding the image.

For production, PostgreSQL with pgvector is supported as a drop-in replacement for SQLite and ChromaDB. Swap PEBBLE_STORAGE_BACKEND=postgres and set PEBBLE_DATABASE_URL in your .env.

Minimum requirements

  • Python 3.13
  • A Discord bot token with message_content and members intents enabled
  • An OpenRouter API key (used for all LLM and embedding calls)
  • Your Discord user ID (enables intimate/DM mode and owner-only commands)
DISCORD_BOT_TOKEN=...
OPENROUTER_API_KEY=...
PEBBLE_OWNER_USER_ID=...

Everything else is configured in config.yml and takes effect immediately on save.


Configuration

config.yml is the single source of truth for non-secret settings. Environment variables always override it. Hot-reloadable fields update in place; a small number of fields (database backend, embedding model, log format) require a restart.

Notable settings:

Field Default Notes
llm.primary_model openrouter:anthropic/claude-sonnet-4.6 Used for social and intimate agents
llm.cheap_model openrouter:anthropic/claude-haiku-4.5 Router, reflection, scheduled messages
memory.working_memory_size 40 Messages buffered per channel
memory.reflection_interval 15 Messages between background reflection runs
proactive.delurk_enabled false Allow the bot to jump in without being @mentioned
proactive.delurk_probability 0.95 Post-router probability roll for uninvited responses
proactive.daily_reflection_enabled false Run nightly deep reflection

Owner commands

A set of ! commands is available to the configured owner in any channel:

Command Effect
!memory Print the current state of all core memory blocks
!reflect Force a reflection run on the next message
!setmood <mood> Override the bot's current mood
!setblock <label> <content> Directly set a core memory block
!facts [user_id] List stored archival facts
!episodes Show recent episode summaries for the current channel
!profile <user_id> Show a user's profile and notes
!channels Show the channel context directory
!schedule add <type> <HH:MM> "<label>" <prompt> Create a scheduled message
!schedule list List active scheduled events
!dailyreflect Run the daily reflection immediately

Observability

Pydantic Logfire is supported for full agent tracing: every LLM call, tool invocation, and token count is visible in the Logfire web UI. Set LOGFIRE_TOKEN in your .env and observability.logfire_enabled: true in config. Without it, structured logs go to stdout in either JSON (production) or human-readable (development) format.


Caveats

Pebble is a personal project and the codebase reflects that. The intimate mode prompt ships with a fairly specific personality that you will want to replace. The default social mode prompt uses casual language including slurs in a friend-group context, which is intentional for the original use case but will need editing for most deployments. Both are in prompts/ and override the in-code defaults on load.

The memory system is eventually consistent. Compaction runs in the background, so the agent may not yet have access to a fact that was just said until the next reflection cycle. For time-sensitive information, the agent can call memory write tools directly during a response.

Vector search requires an OpenRouter API key with access to an embedding model. Without it, all semantic search falls back to keyword or chronological queries.


License

AGPL-3.0. If you deploy a modified version on a publicly accessible server, the AGPL requires you to make the modified source available to users of that server.

About

Advanced AI-powered Discord bot persona

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages