Standalone worker for the Smarter Dev proactive Discord agent.
The Discord bot remains responsible for Gateway events, message batching, and watcher classification. This service consumes guild-specific Redis wake streams, maintains one isolated logical agent per guild, uses the application REST API for durable data, and uses Discord REST for Discord operations.
The directory is an independently installable project and is intended to be
split into its own repository. It must not import the parent smarter_dev
package.
uv sync --group dev
uv run pytest
uv run proactive-agentRequired runtime variables are documented in .env.example.
The service deploys as its own smarter-dev-proactive-agent Deployment in the
existing smarter-dev DigitalOcean Kubernetes namespace. It shares Redis and
the Discord bot identity with the bot, reaches the application through the
internal API_BASE_URL, and receives no database credentials. No Kubernetes
Service or ingress is required because the worker only makes outbound calls;
the kubelet probes its health port directly.
The repository CI workflow tests every change. A successful push to main
builds zzmmrmn/smarter-dev-proactive-agent:<commit>, pushes it to Docker Hub,
applies deploy/kubernetes.yaml, and waits for rollout completion. Configure
the same repository Actions secrets used by the bot deployment:
DIGITALOCEAN_ACCESS_TOKENDOCKER_USERNAMEDOCKER_PASSWORD
Before the first deployment, add this key to the existing
smarter-dev-secrets Kubernetes Secret:
proactive-agent-api-key: a distinct Skrift key withbot-apipermission
The manifest continues to use the existing redis-url, discord-bot-token,
provider-key, and brave-search-api-key entries. anthropic-api-key becomes
ANTHROPIC_API_KEY: any claude-* model id (for example
PROACTIVE_AGENT_MODEL=claude-opus-5-5) calls Anthropic directly with it,
with 1-hour prompt caching on the tools, system prompt and newest message.
The defaults are claude-opus-5-5 for the agent and claude-haiku-5-5 for
the web summarizer, which caches only its system prompt since each page is
read once. OpenRouter's anthropic/* ids are refused at startup. Production model calls use
those direct provider credentials; LiteLLM remains a local-development option.
Deploy the additive application migration and REST endpoints from the bot
repository before deploying this worker. Keep the bot in embedded mode until
the worker is ready; a running worker cannot acquire a guild unless the bot has
marked that guild external in Redis.
- The Discord bot is the sole Gateway consumer and owns watcher cadence.
- A forum switched on as a whole has one settings row, the forum's. The bot
gates its posts (a post inherits the forum's row unless it has its own);
the worker admits each post a wake names as a channel of its own, never
the forum itself, and every post shares the forum's watch instructions.
The worker asks the application for forum rows (
?forums=true); one that does not is never shown them. While running it keepsproactive:v1:capability:forum-postsset (3-minute expiry), and the bot routes a forum post's messages to the worker only while that key is set. Those messages travel on keys of their own:proactive:v1:forum-ready, and per guildforum-wake,forum-pendingandforum-pending-dropped. A forum-post wake is a wake of its own (its id, claimed context, retry counter and checkpoints come from those keys alone); this worker runs it after the shared keys' wake, under the same guild lease. An older replica still running mid-rollout never reads them, so it cannot take a post's wake and drop it, nor strand one's state by finishing the shared wake. A post with its own enabled row stays on the shared keys, as before. - Waking envelopes go to
proactive:v1:{guild:<id>}:wake; non-waking context waits in the guild's bounded pending list until the next wake. - Workers use consumer groups and a guild lease, so replicas process different guilds concurrently while one guild remains serialized.
- The bot writes a per-guild Redis ownership fence at startup and on every
notification. Workers can acquire or renew a lease only while that guild is
marked
external; rollback toembeddeddrains stale external wakes without executing their side effects. - Discord actions use the same bot token through Discord REST. All durable application data is read or written through the authenticated application API; the worker has no database credentials.
- Agent history is cached immediately in Redis and flushed to Postgres through the API after the configured debounce. Shutdown attempts a final flush.
- A model HTTP 503 during a wake is retried on the same guild lease after 30, 60, then 120 seconds. If it still fails, the wake is dead-lettered. The worker adds one history-only note with the retry count and outcome, so the agent sees it on its next real wake without generating another wake.
- Notification envelopes carry Discord message text, so a wake that finishes
(or is dead-lettered after its last attempt) acknowledges and deletes its
wake-stream entries and deletes its claimed batch at once. A failed attempt
with attempts left keeps both for the retry. A batch still expires 6 hours
after its oldest envelope was written, the backstop for a wake that never
finishes; its dropped counter, a number only, 6 hours after the claim. The dead-letter stream (
proactive:v1:dead-letter) keeps only the notifications' ids and the error's type, trimmed to 48 hours on every write.
- Deploy the history API migration and this worker with the bot still in
embeddedmode. - Put a test guild in
PROACTIVE_AGENT_SHADOW_GUILD_IDS. The bot continues to own side effects while publishing the exact Redis envelopes for inspection. - Move that guild to
PROACTIVE_AGENT_EXTERNAL_GUILD_IDS. This disables its embedded consumer and makes the worker the only side-effect owner. - Compare response/reaction counts, queue lag, dead letters, history revisions, and Discord-visible behavior before expanding the comma-separated guild list.
- After every guild is external, set
PROACTIVE_AGENT_EXECUTION_MODE=external.
PROACTIVE_AGENT_EMBEDDED_GUILD_IDS provides a per-guild rollback override
after the global mode is external. The embedded, shadow, and external guild
lists are mutually exclusive; the bot refuses an ambiguous configuration.
The bot and worker must share Redis, DISCORD_BOT_TOKEN, and an API key with
the existing bot-api permission. Rolling back is the reverse flag change;
pending Redis wakes and persisted guild history remain available.
For an immediate per-guild rollback, remove the guild from
PROACTIVE_AGENT_EXTERNAL_GUILD_IDS, add it to
PROACTIVE_AGENT_EMBEDDED_GUILD_IDS, and redeploy the bot. Do not place a guild
in more than one list. The bot refreshes the Redis ownership fence before its
embedded consumer can resume side effects.
This directory has no imports from the parent application and can become the root of its own repository with history preserved:
git subtree split --prefix=services/proactive-agent -b proactive-agent-main
git push <new-repository> proactive-agent-main:mainThe canonical notification and control schemas must be released in lockstep with the producer. Versioned models reject unknown fields to prevent silent contract drift.