A Discord-first AI agent powered by Agno, with streaming responses, vector knowledge base, self-improvement, team orchestration, and a rich toolkit ecosystem.
- Streaming responses — real-time message updates in Discord as the agent thinks
- Agno toolkits — WebSearch, HackerNews, Website, GitHub, YouTube, File, Shell, Reddit, Discord
- Agno Teams — native team orchestration with member delegation
- Team observability — logs selected members, delegation paths, run timing, and metrics
- Mention-first — responds when tagged with
@Bot
- Memory — SQLite-backed with automatic updates, session summaries, and chat history
- Cognee memory (optional) — long-term external memory with conversation chunking, metadata-rich ingestion, and retrieval fallback across payload/path variants
- Learning — Agno LearningMachine per agent (user profile, memory, session context, entity, learned knowledge)
- Knowledge base — vector search with LanceDb (file-based) or PgVector (PostgreSQL) backends
- Self-improvement — discovery tools let the agent save and search its own learnings over time
- Soul + Heartbeat — persona via
workspace/SOUL.md, proactive heartbeat viaworkspace/HEARTBEAT.md
- Docker support — optional PostgreSQL + PgVector and Cognee via
docker-compose.yml - Structured logging — config-driven stdout + optional rotating file logs
- Tool permissions — runtime allow/deny rules with JSONL audit logging
- Extensible — add agents via config or folder-based workspace agents
# Using uv (recommended)
uv sync
# Or with pip
pip install -e .python scripts/setup_bot.pyThe wizard creates ~/.bitdoze-bot/ (or $BITDOZE_BOT_HOME) with:
config.yaml— main configuration.env— secrets (tokens, API keys)workspace/— SOUL.md, AGENTS.md, USER.md, HEARTBEAT.md, CRON.yamlworkspace/agents/— folder-based agent definitions, including starter general subagents for delegationworkspace/knowledge/— documents for the knowledge baseskills/,logs/,data/bitdoze-bot.service— systemd unit file
Wizard prompts:
- Required: Discord bot token
- Optional: GitHub token, model/API settings, systemd service install
- Optional: PostgreSQL + PgVector setup via Docker (for knowledge base)
- Optional: Knowledge backend selection (LanceDb or PgVector)
If you chose PgVector/Cognee during setup, or want to start them manually:
docker compose up -dThis starts:
- PostgreSQL 17 with pgvector on port
5532, storing data at~/.bitdoze-bot/data/pgdata - Cognee API on
127.0.0.1:8000(host-loopback only), storing data at~/.bitdoze-bot/data/cognee
# Initialize and load documents from workspace/knowledge/
python scripts/setup_knowledge.py
# Or with custom options
python scripts/setup_knowledge.py --backend pgvector --docs-dir /path/to/docsAdd .md, .txt, or .pdf files to ~/.bitdoze-bot/workspace/knowledge/ and re-run the script to ingest them.
python scripts/generate_soul.py
# or force overwrite: python scripts/generate_soul.py --forceGenerates a comprehensive personality and self-improvement template at ~/.bitdoze-bot/workspace/SOUL.md.
$EDITOR ~/.bitdoze-bot/.env
$EDITOR ~/.bitdoze-bot/config.yaml
python main.py- Enable the Message Content Intent in the Discord Developer Portal for your bot.
- Invite the bot with the right permissions to post messages in your target channels.
Edit ~/.bitdoze-bot/config.yaml (or config.yml if that is your active file):
- Relative paths are resolved from the config file location.
- Override home location with
BITDOZE_BOT_HOME=/custom/path. - Explicit overrides still work:
python main.py --config /path/config.yaml --env-file /path/.env.
model: provider, model id, base URL, and API key env varmodel.structured_outputs: setfalsefor providers that reject response_format (e.g., StepFun/OpenRouter)
discord: bot token env vardiscord.access_control: optional ingress allowlists (allowed_user_ids,allowed_channel_ids,allowed_guild_ids,allowed_role_ids)runtime.streaming_enabled: stream responses to Discord with real-time message edits (default:true)runtime.streaming_edit_interval: seconds between message edits during streaming (default:1.5)
Streaming progressively edits the Discord message as the agent generates content. It falls back to non-streaming for team runs and research mode.
runtime: timeouts for agent runs, cron, heartbeat, and max concurrencyruntime.slow_run_threshold_seconds: sends an interim "still working" reply when complex runs take longer than expectedruntime.session_id_strategy: session partitioning for runs/history (channel,user,channel_user; default:channel_user)
knowledge.enabled: activate vector search knowledge baseknowledge.backend:lancedb(file-based, zero setup) orpgvector(requires PostgreSQL)knowledge.embedder: embedding model (default:text-embedding-3-small)- LanceDb settings:
lance_uri,table_name,learnings_table_name - PgVector settings:
db_url(orPGVECTOR_DB_URLenv var)
knowledge:
enabled: true
backend: lancedb # or pgvector
embedder: text-embedding-3-small
lance_uri: data/lancedb
table_name: bitdoze_knowledge
learnings_table_name: bitdoze_learnings
# db_url: postgresql+psycopg://bitdoze:secret@localhost:5532/bitdoze # for pgvectorThe agent can build and query its own knowledge base using the discoveries toolkit:
save_discovery: save a reusable learning (corrections, preferences, patterns)search_discoveries: search past learnings before answering
Add discoveries to an agent's tools list to enable:
agents:
definitions:
- name: main
tools: [web_search, website, file, discoveries]Combined with learned_knowledge: agentic in learning config, the agent decides when to save and recall learnings automatically.
memory:mode: automatic(best capture), backend selection (sqliteorpostgres), history + summaries (custom prompt supported)- SQLite settings:
backend: sqlite+db_file - Postgres settings:
backend: postgres+db_url(orMEMORY_DB_URL, fallbackPGVECTOR_DB_URL) memory.summary_prompt: customizable session summary with support fordecisionsandunresolvedkeysmemory.cognee: optional external long-term memory (Cognee API)memory.cognee.auto_sync_conversations: when true, each successful Discord user/assistant turn is stored in Cognee as a compact summary plus chunked user/assistant entries (better semantic recall on long turns)memory.cognee.auto_recall_enabled: when true, each incoming message performs Cognee recall and injects top matches into system contextmemory.cognee.auto_cognify_after_write: when true, the bot triggers Cogneecognifyafter successful writes (throttled) so new facts become searchable- Cognee ingestion includes metadata (user/session/agent/channel/guild/timestamps), plus duplicate suppression for recently repeated content
- Cognee retrieval tries multiple compatible payload/path shapes and skips empty 2xx responses to reduce false misses
Recommended Cognee config:
memory:
cognee:
enabled: true
base_url: http://localhost:8000
user: bitdoze-bot@example.com
dataset: bitdoze-user-profile
auto_sync_conversations: true
auto_recall_enabled: true
auto_recall_limit: 5
auto_recall_timeout_seconds: 3
auto_recall_max_chars: 2000
auto_recall_inject_all: false
auto_cognify_after_write: true
cognify_cooldown_seconds: 60
timeout_seconds: 8
max_turn_chars: 6000
auth_token_env: COGNEE_API_TOKENUse this quick checklist when memory does not appear to work:
-
Verify config is enabled:
memory.cognee.enabled: truememory.cognee.auto_sync_conversations: truememory.cognee.auto_recall_enabled: true
-
Verify Cognee API is reachable:
curl -sS http://localhost:8000/api/v1/datasetsExpected: JSON response (list/object), not connection refused.
-
Verify ingest is happening:
- Send one Discord message to the bot and wait for the reply.
- Check logs for success lines containing:
Cognee add_memory successCognee save_conversation_turn status=ok
-
Verify recall is happening:
- Ask a follow-up question that references the previous message.
- Check logs for:
Cognee search successCognee auto-recall injected items=...
-
If recall is weak:
- Increase
memory.cognee.auto_recall_limit(for example7to10). - Increase
memory.cognee.auto_recall_timeout_secondsif Cognee search is timing out. - Increase
memory.cognee.auto_recall_max_chars(for example2500to4000). - Set
memory.cognee.auto_recall_inject_all: trueto inject full matched items (no per-item truncation and no total char cap). - Ensure your question includes clear keywords from the earlier memory.
- Increase
Notes:
- Dataset creation retries automatically after transient failures.
- Conversation turns are stored as a summary plus chunks for better semantic retrieval on long messages.
- Recent identical memory payloads are deduplicated to reduce noise.
learning: enable Agno LearningMachine and set learning modes (always,agentic,propose,hitl)learning.stores.learned_knowledge: agentic— agent decides when to save/search learnings (recommended)
monitoring: JSONL telemetry for runs + heartbeat watchdog alerts for long-running active taskstool_fallback.denied_tools: tool names blocked during XML-style fallback execution (default:shell,discord)logging: set level, format, and rotating file settings from YAMLtool_permissions: runtime allow/deny rules for tool use plus JSONL audit loggingtoolkits: enable/disable web search, hackernews, website, github, youtube, file, shell, reddit, discord, cognee toolsagents.workspace_dir: folder-based agent loading fromworkspace/agents/<name>/teams: native Agno Team definitions, delegation behavior, team memory options, and default teamheartbeat: 30-min cadence, optional channel override,session_scope(isolatedto avoid heartbeat history growth), and optional dedicatedagentcron: schedule jobs viaworkspace/CRON.yamlagents: define multiple agents, per-agent tool selections, and routing rulesskills: optional skill packs loaded fromskills/context: enable datetime injection and set timezonecontext.use_workspace_context_files: whenfalse, skipUSER.md/daily logs/MEMORY.mdinjection and rely on Agno DB memory+learning onlycontext.agents_path: load workspace instructions into system contextcontext.user_path+context.memory_dir+context.long_memory_path: load USER + daily memory + long memorycontext.main_session_scope:dm_only(default) oralwaysfor long memorycontext.scope_workspace_context_by_tenant: isolate workspace context files by guild/user (default:true)context.scoped_context_dir: root folder for tenant-isolated context files (default:workspace/context)context.allow_global_context_in_guilds: iffalse, global USER/MEMORY files are not injected in guild messages when tenant scoping is disabled
Configure learning in config.yaml:
learning:
enabled: true
mode: always # default mode for enabled stores
stores:
user_profile: true
user_memory: true
session_context: alwaysFor each store, you can use:
true/false- a mode string:
always,agentic,propose,hitl - an object with
{ enabled, mode, ... }for advanced per-store options
By default, the bot replies with the default agent. You can add routing rules:
agents:
routing:
rules:
- agent: research
channel_ids: [123456789012345678]
contains: ["research:"]Rules match on channel_ids, user_ids, guild_ids, contains, and starts_with.
All specified conditions in a rule must match.
- Configure runtime tool access with
tool_permissions. - Supported selectors per rule:
channel_idsrole_idsuser_idsguild_idsagentstools
- Rule resolution is deterministic:
denyoverridesallow- if no rule matches,
default_effectis applied
- Blocked tool calls return a clear user-facing message.
- Every tool event is written to append-only JSONL audit logs with outcomes:
allowedblockedexecutedfailed
- Audit applies to normal Discord runs, fallback tool-call execution, cron, and heartbeat runs.
- Argument logging is off by default. If enabled, configured sensitive keys are redacted.
Example:
tool_permissions:
enabled: true
default_effect: allow
rules:
- effect: deny
tools: [shell]
- effect: allow
tools: [shell]
role_ids: [123456789012345678]
channel_ids: [234567890123456789]
audit:
enabled: true
path: logs/tool-audit.jsonl
include_arguments: false
redacted_keys: [token, secret, password, api_key, authorization]Agents can be added without code changes using:
workspace/agents/<agent-name>/agent.yamlworkspace/agents/<agent-name>/AGENTS.md
With the default home setup, these paths resolve under ~/.bitdoze-bot/workspace/agents/.
Example agent.yaml:
name: software-engineer
enabled: true
model:
id: glm-4.7
base_url: https://api.z.ai/api/coding/paas/v4
api_key_env: GLM_API_KEY
tools: [web_search, website, github, file, shell]
skills: []Folder agents are merged with config-defined agents by name. If names collide, folder definitions win.
The starter setup includes general-subagent, general-subagent-2, and general-subagent-3 as low-context workers, plus an example general-subagents team for parallel delegation.
Example:
teams:
default: delivery-team
definitions:
- name: delivery-team
members: [architect, software-engineer]
respond_directly: true
determine_input_for_members: true
delegate_to_all_members: true
add_team_history_to_members: true
num_team_history_runs: 5The team and members share the configured SQLite DB. Team memory/history is handled by Agno Team settings, while member learning is handled by each member's LearningMachine config.
- Each team/agent run logs:
- target kind (
agentorteam) and target name - selected team members
- elapsed runtime
- run id + model
- token/latency metrics when provided by Agno
- delegation paths extracted from
member_responses
- target kind (
- Logging is configured from
config.yamlunderlogging. - Defaults:
level: INFO,format: detailed, file logging enabled atlogs/bitdoze-bot.log. - Invalid log levels safely fall back to
INFO.
Example:
logging:
level: DEBUG
format: detailed # detailed | simple | custom format string
file:
enabled: true
path: logs/bitdoze-bot.log
max_bytes: 10485760
backup_count: 5- Live tail:
tail -f ~/.bitdoze-bot/logs/bitdoze-bot.logEach agent.run() call is guarded by a configurable timeout. This applies per call, not per
full flow — a research mode request that retries once gets the timeout applied to each attempt
independently.
runtime:
agent_timeout: 600 # seconds per agent.run for Discord messages and research
cron_timeout: 600 # seconds per agent.run for cron jobs
heartbeat_timeout: 120 # seconds per agent.run for heartbeat
max_concurrent_runs: 4 # max parallel agent.run calls across all sources- If a run exceeds its timeout the bot replies with a timeout message (Discord) or logs a warning (cron/heartbeat) and moves on.
max_concurrent_runslimits how manyagent.runcalls can execute in parallel across Discord messages, cron, and heartbeat combined.- All values are optional and fall back to the defaults shown above when omitted.
Runtime flow:
- On startup, the bot auto-loads
~/.bitdoze-bot/config.yaml(legacy fallback:config.yml, then repoconfig.yaml), then builds global toolkits fromtoolkits. - It loads agents from:
agents.definitionsin configworkspace/agents/<name>/agent.yaml(folder-based agents)
- For folder-based agents,
workspace/agents/<name>/AGENTS.mdis added to that agent's instructions. - It builds Agno
Agentmembers (with memory + learning), then AgnoTeamobjects fromteams.definitions. - The runtime registry can resolve both agents and teams by name, including aliases.
Message handling:
- Discord message arrives -> routing rules in
agents.routing.ruleschoose a target name. - The target can be either a single agent or a team.
- The bot calls
.run(...)on the selected target. - If target is a team, Agno handles delegation and synthesis natively.
Memory and learning:
- Shared DB:
memory.backend=sqlite+memory.db_fileormemory.backend=postgres+memory.db_url. - Member learning: configured via
learning(LearningMachine stores likeuser_profile,user_memory). - Team memory/history: configured in
teams.definitions[]via options such as:add_team_history_to_membersnum_team_history_runsadd_history_to_context- session summary settings inherited from memory config.
Add a new teammate:
- Create folder:
workspace/agents/<new-agent>/ - Add
agent.yamlwith model settings (id,base_url,api_key_env) - Add
AGENTS.mdwith role-specific instructions - Add the agent name to
teams.definitions[].membersinconfig.yaml - Restart the bot
To reduce prompt pressure on the main agent, prefer a low-context worker pattern:
- Keep
mainas the coordinator - Delegate bounded tasks to
general-subagent - Use a small worker team such as
general-subagentswhen the task can be split into parallel chunks
Skills follow Agno's skill structure (see Agno docs). Each skill lives in its own folder
under skills/ with a SKILL.md that includes YAML frontmatter (name/description).
The agent loads skills via LocalSkills.
With the default home setup, this resolves under ~/.bitdoze-bot/skills/.
To target specific skills per agent, set:
agents:
definitions:
- name: research
skills: [web-research]Skill names must be lowercase and use hyphens, and must match the folder name.
The project includes a docker-compose.yml for PostgreSQL 17 with pgvector and a Cognee API service. This is optional — LanceDb works without any external services.
# Start services
docker compose up -d
# Check status
docker compose ps
# View logs (examples)
docker compose logs -f pgvector
docker compose logs -f cognee
# Stop
docker compose downPgVector configuration:
| Setting | Default | Env Var |
|---|---|---|
| Port | 5532 | PGVECTOR_PORT |
| Database | bitdoze | — |
| User | bitdoze | — |
| Password | bitdoze_secret | POSTGRES_PASSWORD |
| Data dir | ~/.bitdoze-bot/data/pgdata |
BITDOZE_BOT_HOME |
| Connection URL | postgresql+psycopg://bitdoze:bitdoze_secret@localhost:5532/bitdoze |
PGVECTOR_DB_URL |
Port 5532 is used to avoid conflicts with any system PostgreSQL on 5432.
Cognee configuration:
| Setting | Default | Env Var |
|---|---|---|
| Host bind | 127.0.0.1:8000 |
— |
| Require auth | false |
— |
| Backend access control | false |
— |
| LLM key passthrough | empty | LLM_API_KEY |
| Data dir | ~/.bitdoze-bot/data/cognee |
BITDOZE_BOT_HOME |
| Script | Purpose |
|---|---|
scripts/setup_bot.py |
Interactive setup wizard (config, env, service, Docker) |
scripts/generate_soul.py |
Generate/update SOUL.md with self-improvement template |
scripts/setup_knowledge.py |
Initialize knowledge base and load documents |
# Setup wizard
python scripts/setup_bot.py
# Generate SOUL.md (supports --force, --dry-run, --home-dir)
python scripts/generate_soul.py
# Setup knowledge base (supports --backend, --docs-dir, --config)
python scripts/setup_knowledge.py- Heartbeat sends a proactive update every 30 minutes. If it returns
HEARTBEAT_OK, the message is suppressed. - For lower token usage, keep
heartbeat.session_scope: isolatedand optionally pointheartbeat.agentto a lightweight agent with minimal tools/memory. tools: []on an agent/team now means "no tools" (explicitly empty), not "all tools".- FileTools is sandboxed to
workspace/by default.
Enable cron jobs in workspace/CRON.yaml:
enabled: true
timezone: Europe/Bucharest
channel_id: 123456789012345678
jobs:
- name: daily-status
cron: "0 9 * * *"
agent: main
message: "Send a daily status update."
deliver: true
session_scope: isolatedRun:
uv run pytest -qCurrent coverage includes:
- workspace agent loading + team registry wiring
- alias resolution
- routing rule selection
- delegation path extraction helper
- config loading and validation
- setup wizard answer generation
bitdoze-bot/
├── main.py # Entry point
├── config.example.yaml # Reference configuration
├── docker-compose.yml # PgVector + Cognee (optional)
├── pyproject.toml # Dependencies (managed with uv)
├── bitdoze_bot/
│ ├── agents.py # Agent/team construction + knowledge base
│ ├── config.py # Config loading and resolution
│ ├── cron.py # Scheduled job runner
│ ├── discord_bot.py # Discord client + streaming handler
│ ├── discovery_tools.py # Self-improvement tools (save/search discoveries)
│ ├── heartbeat.py # Periodic health checks
│ ├── logging_setup.py # Structured logging
│ ├── run_monitor.py # Run monitoring + telemetry
│ ├── setup_wizard.py # Interactive setup
│ ├── tool_permissions.py # Tool access control + audit
│ └── utils.py # Shared utilities
├── scripts/
│ ├── setup_bot.py # Setup wizard entry point
│ ├── generate_soul.py # SOUL.md generator
│ └── setup_knowledge.py # Knowledge base setup
└── tests/ # pytest test suite