Skip to content

danieletorelli/golemclaw

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GolemClaw

A scaled-down OpenClaw — a Telegram-based personal AI assistant — built on Golem 1.5, designed to run locally or on Golem Cloud.

GolemClaw is a chatty multi-agent backend: every user gets their own durable "brain" that remembers facts about them across turns, sets timezone-aware reminders, looks up the weather and the web, sends emails, schedules events with conflict detection, and even tracks social commitments between users. The bot understands natural language (thanks to an LLM with native function calling) but slash commands are always available as an explicit-control fallback.


Table of contents

  1. Highlights
  2. Example session
  3. Architecture
  4. Tools
  5. Build and run locally
  6. Deploy to Golem Cloud
  7. Configuration
  8. Tests and CI
  9. Golem 1.5 features in use

Highlights

  • Durable multi-agent architecture — every per-user agent (brain, reminders, mail, LLM router, social commitments) is an #[agent_definition] on Golem 1.5. State persists across crashes, restarts, and replays via Golem's durable execution oplog; scheduled invocations (reminder firings, commitment nudges, outbox polling) survive worker restarts.
  • Natural-language tool dispatch with agentic multi-turn — every free-form message is fed to the LLM with the full tool catalogue exposed via native function calling (Gemini functionDeclarations, OpenRouter tools). The brain runs an agentic loop, up to 3 LLM steps per user message: the model can emit fetch tools (reminders_list, events_list, commitments_list, weather, search), the brain feeds the actual results back as functionResponse / tool-role messages, and the model composes a follow-up that uses the fetched data. Common patterns like "cancel my pizza reminder" or "send me my events as email" resolve in one user turn. Fetch tools (reminders_list, events_list, commitments_list, weather, search) can trigger follow-up LLM steps; action tools (remember_fact, forget_fact, remind, cancel_reminder, schedule, cancel_event, send_email, commitment_create, commitment_action) return user-facing output.
  • Learned (and forgotten) facts from chat — when the user shares personal details the model calls remember_fact(key, value). Facts are sanitized server-side and persisted durably per user, so the bot stays consistent across turns and survives chat resets. When a valid timezone fact is remembered, reminder/scheduling parsing switches to that IANA timezone immediately. When the user asks to forget something — in any language ("forget my name", "dimentica il mio nome", "I moved away from Riverdale") — the model calls forget_fact, removing the relevant fact (or multiple at once) cleanly.
  • Location messages — tap the Telegram paperclip → Location → "Send my current location" and the bot reverse-geocodes (Nominatim for the city, Open-Meteo for the IANA timezone) and persists city / country / timezone as facts. When a weather request is missing location text, the bot replies with a one-tap "📍 Share my location" reply keyboard.
  • Timezone-aware reminders — time expressions like tomorrow 18:00 are anchored to the user's IANA timezone (Europe/Rome, America/New_York, …) instead of UTC. Backed by chrono-tz with a layered fallback (Open-Meteo → country table → longitude approximation) so the bot always produces a usable zone. Override with /timezone Europe/Rome at any time.
  • Six tools — Reminders, Weather, Web search, Email, Shared Scheduling with conflict detection, and Social Commitments (a cross-user durable workflow with accept / decline / done / snooze and overdue escalation).
  • Polished Telegram UX — every reply is HTML-formatted (bold labels, monospace IDs, clickable @handle mentions). Long /reminders / /events / /commitments lists paginate with « Prev / Next » inline buttons AND render per-item ✖ Cancel / ✓ Done buttons so users tap to act without typing ids. The slash commands register with Telegram via setMyCommands so they show up in the / autocomplete drawer. The bot shows a "typing…" indicator while the LLM is thinking, and acknowledges inline-button taps immediately so the loading spinner never lingers.
  • Free-tier-first — keyless out-of-the-box for Weather (Open-Meteo) and Search (Wikipedia fallback). When no LLM key is set, the bot still responds with a clean provider: echo template. Add Gemini Flash, OpenRouter, Brave, Serper, or Resend keys to upgrade quality.
  • Local + shared memory — per-user state lives in UserBrainAgent; global coordination (user directory, shared event calendar, commitment ledger, RBAC store, notification outbox) lives in a singleton SharedMemoryAgent() agent.
  • Golem 1.5 in depth — singleton + per-user agents, scheduled invocations, retry policies, resource quotas, atomic blocks (for Resend), custom application spans, MCP server with #[prompt] / #[description] annotations on exposed tool methods, custom HTTP API mounts, RBAC with first-user-admin bootstrap, explicit snapshotting = "disabled" on all agent definitions.
  • Fully tested — 179 host unit tests plus a self-contained end-to-end harness that exercises the full bot pipeline against a mock Telegram API server. Same suite runs on every push and pull request in GitHub Actions.

Example session

A short conversation showing what a typical chat with the bot looks like once the LLM is configured (Gemini Flash here):

You    Hi, I'm Alex in Berlin.
Bot    Got it — noted your name=Alex, city=Berlin, timezone=Europe/Berlin.
       I'll use this for reminders and schedules.

You    How's the weather near me?
Bot    Weather in Berlin, Germany: 11.4 C, wind 10 km/h, partly cloudy.

You    Remind me to call mom tomorrow at 6pm
Bot    Reminder 9a83…b51 scheduled for 2026-05-16T18:00:00+02:00 Europe/Rome
       No conflicts detected.

You    What can you tell me about Apache Ignite?
Bot    Top results for "Apache Ignite":
       1. Apache Ignite — Distributed Database for High-Performance …
       2. …

You    /me
Bot    Here is what I remember about you:
       - preferred name: Alex
       - timezone: Europe/Berlin
       - reminders: 1
       Learned from chat:
       - city = Berlin
       - name = Alex
       - timezone = Europe/Berlin

You    [📎 → Location → "Send my current location"]
Bot    📍 Location noted: Berlin, Germany.
       Timezone: Europe/Berlin
       Coordinates: 52.5200, 13.4050

You    /commit @friend | tomorrow 18:00 | send insurance card
Bot    📋 Commitment created for @friend and awaiting acceptance.
       (@friend receives an inline keyboard with ✅ Accept / ❌ Decline)

That single session exercises memory of learned facts, geocoded weather lookup, timezone-aware durable reminders, web search with provider fallback, native Telegram location-share with reverse geocoding, the /me identity card with provenance, and the cross-user Social Commitments workflow.


Architecture

                Telegram
           webhook POST   │   outbound long-poll
                   │      │      │
                   ▼      │      ▼
        TelegramGatewayAgent("main")   TelegramPollerAgent("main")
                   │      │      │
                   └──────┴──────┘
                          │
                     process_update
                   │
                   ▼
        UserBrainAgent(<user_id>)
        │   │   │   │   │   │
        │   │   │   │   │   └─► CommitmentAgent(<id>)
        │   │   │   │   └─────► MailAgent(<user_id>)         (atomically_async)
        │   │   │   └─────────► LlmRouterAgent(<user_id>)    (Gemini → OpenRouter → Echo)
        │   │   └─────────────► ReminderAgent(<user_id>) ─schedule_fire_reminder─► itself
        │   └─────────────────► SharedMemoryAgent()
        └─────────────────────► tool_clients (Open-Meteo, Wikipedia, Brave, Serper)

Key properties:

  • Per-user agents are keyed by Telegram user_id. Two users get two independent durable workers for each role.
  • Shared coordination lives in the singleton SharedMemoryAgent() that holds the user directory, RBAC store, shared event calendar, commitment summaries, and a notification outbox.
  • Outbox pattern: agents enqueue notifications; the gateway and poller both drain and deliver via Telegram. This decouples inbound update processing from outbound delivery and keeps everything replay-safe.
  • Every cross-agent RPC is durable. Scheduled invocations (reminder fires, commitment nudges, due/escalation checks, outbox polling) survive crashes, restarts, and replay.

Source layout

Path Purpose
src/models.rs All Schema-derived DTOs shared across agents (incl. ToolCall, ToolArg for native function calling)
src/commands.rs Telegram command parser and ParsedCommand enum
src/tool_catalog.rs Single source of truth for the 11 LLM-callable tools — name, description, args, required capability
src/domain.rs Pure domain logic (tool-call validation, conflict detection, capability mapping, commitment state machine, reminder fire policy, search-provider selection, LLM strategy selection)
src/messages.rs All user-facing strings
src/time_utils.rs Time parsing and formatting with IANA timezone support (chrono-tz): RFC3339, tomorrow HH:MM, today HH:MM, YYYY-MM-DD HH:MM, relative seconds/minutes/hours/days
src/tool_clients.rs HTTP clients for Open-Meteo, Brave, Serper, and the keyless Wikipedia fallback
src/config.rs Typed AppConfig with optional fields and secret fields, plus a telegramApiBaseUrl override used by tests
src/telegram_gateway_agent.rs Webhook ingress, outbound sendMessage, outbox poller
src/telegram_poller_agent.rs Local-dev long-poll transport (getUpdates) that feeds the same user-brain pipeline
src/user_brain_agent.rs Command orchestration, local memory, capability gating, custom spans, natural-language tool dispatch
src/shared_memory_agent.rs User directory, RBAC store, events, commitments index, outbox queue
src/reminder_agent.rs Reminder lifecycle and scheduled fire
src/commitment_agent.rs Social Commitments workflow
src/mail_agent.rs Resend integration wrapped in atomically_async
src/llm_router_agent.rs Gemini / OpenRouter / Echo routing — exposes the tool_catalog via native function calling on both providers
golem.yaml Application manifest
tests/integration/ End-to-end harness, mock Telegram, fixtures, cloud smoke
.github/workflows/ci.yml CI workflow

Tools

Tool Trigger Backend Keyless?
Natural-language chat (free-form messages), /llm, /llm model … LLM router: in auto mode it tries Gemini then OpenRouter (if configured). If no LLM key is configured, it returns local echo. In auto, provider failures fall through to the next provider; if all configured providers fail, the user gets an "LLM unavailable" reply. OpenRouter tool-calling is enabled only for models in llm_openrouter_function_calling_models. yes (echo when no provider key)
Memory & timezone (free-form), /me, /timezone <IANA> remember_fact / forget_fact tools persist or remove facts per user; the timezone fact mirrors into the brain's local-time parser (and reverts to the default when forgotten) yes
Reminders (free-form), /remind, /reminders, /cancel_reminder Golem scheduled invocations yes
Weather (free-form), /weather <location> Open-Meteo + geocoding. City, Country inputs are normalized to bare city before geocoding. Empty /weather prompts a one-tap location-share keyboard. yes
Web search (free-form), /search <query> Provider is selected by configured keys: Brave if braveApiKey is set, else Serper if serperApiKey is set, else Wikipedia keyless. (No per-request failover between providers.) yes
Email (free-form), /email to | subject | body Resend (atomic block around HTTP send) no
Shared scheduling (free-form), /schedule, /events, /cancel_event SharedMemoryAgent + conflict detection; paginated /events list renders a per-item ✖ Cancel inline button. Reminder-shadow events (the ±30 min conflict-detection blocks the reminder agent auto-registers) are filtered from /events. /cancel_event on a reminder- id transparently delegates to /cancel_reminder so the user gets the right outcome regardless of which tool they reach for. yes
Social commitments (free-form), /commit, /commitments, /commit_action CommitmentAgent durable workflow with accept / decline / done / snooze / cancel / overdue escalation; the LLM commitment_action tool exposes the same state-machine actions; paginated /commitments list renders per-item ✓ Done / ✖ Cancel inline buttons; canceling notifies the assignee with the creator's mention yes
MCP tools external MCP clients Exposes UserBrainAgent, SharedMemoryAgent, ReminderAgent, CommitmentAgent n/a
RBAC /admin whoami, /admin grant, … First Telegram user becomes admin; capability grants gate every other tool yes

Telegram command reference

/help
/me                                   show what the bot remembers about you
                                       (durable identity + LLM prefs + facts
                                       learned from free-form chat)
/timezone                             show your current IANA timezone
/timezone Europe/Rome                 set your timezone (anchors "tomorrow
                                       18:00" to local time, not UTC)

/remind <when> | <text>
/reminders
/cancel_reminder <id>

/weather <location>
/search <query>
/email <to> | <subject> | <body>

/llm                                show mode and provider models
/llm auto|gemini|openrouter         pick mode
/llm model                          show current Gemini + OpenRouter models
/llm model gemini <model-id>
/llm model openrouter <model-id>
/llm reset-model [gemini|openrouter|all]
/llm models                         list free-model allowlist

/schedule <start> | <end> | <title>
/events

/commit @username | <due> | <title>
/commitments
/commit_action <id> <accept|decline|done|cancel|snooze-<minutes>>

/admin whoami
/admin users                        admin only
/admin grant @user <capability>     admin only
/admin revoke @user <capability>    admin only
/admin make-admin @user             admin only
/admin remove-admin @user           admin only
/admin caps [@user]                 admin only

Capabilities (/admin grant @x <name>): reminders, weather, search, email, llm, scheduling, commitments-create, commitments-read.

The first Telegram user to message the bot is auto-bootstrapped as admin. Subsequent users start with no capabilities and need an admin grant — except they can always respond to a commitment they're a participant of.

OpenRouter free models can be opted in to native function calling by adding their model IDs under agents.LlmRouterAgent.config. llm_openrouter_function_calling_models in golem.yaml (default: google/gemma-4-31b-it:free). Models not on the list still chat normally; they just don't get the tool catalogue, so users fall back on slash commands for those tools.


Build and run locally

Prerequisites:

  • Rust toolchain with the wasm32-wasip2 target
  • golem and golem-cli 1.5.x in PATH
  • curl, jq, python3 (for the integration harness)
cargo fmt --all --check
cargo clippy --target wasm32-wasip2 -- -Dwarnings
cargo test                                       # 179 unit tests
cargo check --target wasm32-wasip2
golem-cli build

# Secrets are populated from env vars via `secretDefaults.local`.
# Required:
export GOLEMCLAW_TELEGRAM_BOT_TOKEN="<TELEGRAM_BOT_TOKEN>"
export GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET="<RANDOM_LONG_STRING>"
# Optional (set empty string to keep keyless fallbacks):
export GOLEMCLAW_LLM_GEMINI_API_KEY=""
export GOLEMCLAW_LLM_OPENROUTER_API_KEY=""
export GOLEMCLAW_BRAVE_API_KEY=""
export GOLEMCLAW_SERPER_API_KEY=""
export GOLEMCLAW_RESEND_API_KEY=""

# Deploy locally
golem-cli -E local deploy --yes

Local default URLs:

  • Telegram webhook: http://golemclaw.localhost:9006/telegram/main/webhook
  • MCP: http://golemclaw.localhost:9007/mcp

Local chat without a public webhook (long-polling)

Real Telegram normally requires a public HTTPS URL it can POST to. For local development you can skip tunnels entirely by using the bundled TelegramPollerAgent, which calls Telegram's getUpdates outbound on a self-scheduling loop. Same brain, same tools, no inbound traffic to your laptop.

No wrapper script is required. Use two terminals:

# Terminal A: start local Golem server
golem server run --router-port 9881 --custom-request-port 9006 --mcp-port 9007

# Terminal B: set env vars + deploy
export GOLEMCLAW_TELEGRAM_BOT_TOKEN="<TELEGRAM_BOT_TOKEN>"
export GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET="<RANDOM_LONG_STRING>"
export GOLEMCLAW_LLM_GEMINI_API_KEY="<GEMINI_API_KEY>"
export GOLEMCLAW_LLM_OPENROUTER_API_KEY="<OPENROUTER_API_KEY>"
export GOLEMCLAW_BRAVE_API_KEY="<BRAVE_API_KEY>"
export GOLEMCLAW_SERPER_API_KEY="<SERPER_API_KEY>"
export GOLEMCLAW_RESEND_API_KEY="<RESEND_API_KEY>"

golem-cli -E local deploy --yes

# Bootstrap/check poller state (also creates the agent if absent)
golem-cli -E local agent invoke 'TelegramPollerAgent("main")' status

# Optional manual controls
golem-cli -E local agent invoke 'TelegramPollerAgent("main")' stop
golem-cli -E local agent invoke 'TelegramPollerAgent("main")' start

In the local environment (componentPresets: debug), polling is enabled by default. Cloud (and any other preset that does not override it) keeps polling disabled by default and uses the webhook path.


Deploy to Golem Cloud

GolemClaw can also run on Golem Cloud. The cloud environment in golem.yaml declares componentPresets: release and HTTP/MCP deployments with explicit domains:

  • Webhook/API domain: claw.apps.golem.cloud
  • MCP domain: claw.mcps.golem.cloud

If those domains are not provisioned in your account, update httpApi.deployments.cloud[0].domain and mcp.deployments.cloud[0].domain in golem.yaml before deploying.

Prerequisites

  • A Golem Cloud account and a golem-cli profile pointing at it. If you haven't done this:

    golem-cli profile new

    Choose the Cloud profile, follow the OAuth flow, and verify with golem-cli profile current and golem-cli account list.

  • A Telegram bot. Talk to @BotFather in Telegram, run /newbot, pick a name and username, and save the bot token.

Steps

Quickstart (minimum required)

# Secrets are populated from env vars via `secretDefaults.cloud`.
export GOLEMCLAW_TELEGRAM_BOT_TOKEN="<TELEGRAM_BOT_TOKEN>"
export GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET="<RANDOM_LONG_STRING>"
export GOLEMCLAW_LLM_GEMINI_API_KEY=""
export GOLEMCLAW_LLM_OPENROUTER_API_KEY=""
export GOLEMCLAW_BRAVE_API_KEY=""
export GOLEMCLAW_SERPER_API_KEY=""
export GOLEMCLAW_RESEND_API_KEY=""

golem-cli build
golem-cli -E cloud deploy --yes

# Must match httpApi.deployments.cloud[0].domain in golem.yaml
export GOLEMCLAW_WEBHOOK_DOMAIN=claw.apps.golem.cloud
curl -X POST "https://api.telegram.org/bot${GOLEMCLAW_TELEGRAM_BOT_TOKEN}/setWebhook" \
  -H "Content-Type: application/json" \
  -d "{
    \"url\": \"https://${GOLEMCLAW_WEBHOOK_DOMAIN}/telegram/main/webhook\",
    \"secret_token\": \"${GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET}\"
  }"

Full setup (recommended)

# 1. (Optional) edit cloud domains in golem.yaml:
#    - httpApi.deployments.cloud[0].domain
#    - mcp.deployments.cloud[0].domain
#
# 2. Set secret env vars consumed by `secretDefaults.cloud`.
#    First two are required. The rest can stay empty for keyless fallback modes.
export GOLEMCLAW_TELEGRAM_BOT_TOKEN="<TELEGRAM_BOT_TOKEN>"
export GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET="<RANDOM_LONG_STRING>"
export GOLEMCLAW_LLM_GEMINI_API_KEY="<GEMINI_API_KEY>"
export GOLEMCLAW_LLM_OPENROUTER_API_KEY="<OPENROUTER_API_KEY>"
export GOLEMCLAW_BRAVE_API_KEY="<BRAVE_API_KEY>"
export GOLEMCLAW_SERPER_API_KEY="<SERPER_API_KEY>"
export GOLEMCLAW_RESEND_API_KEY="<RESEND_API_KEY>"

# 3. Build and deploy:
golem-cli build
golem-cli -E cloud deploy --yes

# 4. Register the Telegram webhook against the deployed domain:
export GOLEMCLAW_WEBHOOK_DOMAIN=claw.apps.golem.cloud
curl -X POST "https://api.telegram.org/bot${GOLEMCLAW_TELEGRAM_BOT_TOKEN}/setWebhook" \
  -H "Content-Type: application/json" \
  -d "{
    \"url\": \"https://${GOLEMCLAW_WEBHOOK_DOMAIN}/telegram/main/webhook\",
    \"secret_token\": \"${GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET}\"
  }"

# 5. Confirm webhook registration:
curl "https://api.telegram.org/bot${GOLEMCLAW_TELEGRAM_BOT_TOKEN}/getWebhookInfo"

# 6. To delete the webhook:
curl -X POST "https://api.telegram.org/bot${GOLEMCLAW_TELEGRAM_BOT_TOKEN}/deleteWebhook"

The first Telegram message you send to the bot bootstraps your user account as the admin (no capabilities are required for admin actions). Subsequent users start with no capabilities; you grant them via /admin grant @user <capability>.


Configuration

Deploy-time environment variables

golem.yaml templates these secret environment variables at deploy time for secretDefaults.local and secretDefaults.cloud. (secretDefaults.test is intentionally fixed for the integration harness.)

Variable Required Used in
GOLEMCLAW_TELEGRAM_BOT_TOKEN yes (local/cloud) secretDefaults.{local,cloud}.telegramBotToken
GOLEMCLAW_TELEGRAM_WEBHOOK_SECRET yes (cloud webhook), recommended otherwise secretDefaults.{local,cloud}.telegramWebhookSecret
GOLEMCLAW_LLM_GEMINI_API_KEY optional secretDefaults.{local,cloud}.llmGeminiApiKey
GOLEMCLAW_LLM_OPENROUTER_API_KEY optional secretDefaults.{local,cloud}.llmOpenrouterApiKey
GOLEMCLAW_BRAVE_API_KEY optional secretDefaults.{local,cloud}.braveApiKey
GOLEMCLAW_SERPER_API_KEY optional secretDefaults.{local,cloud}.serperApiKey
GOLEMCLAW_RESEND_API_KEY optional secretDefaults.{local,cloud}.resendApiKey

For optional secret vars, use an empty string ("") when you want the runtime to keep keyless fallback behavior.

Secrets used by AppConfig (Config<AppConfig> + Secret<String>)

Secret name Typical source
telegramBotToken @BotFather /newbot
telegramWebhookSecret random string ≥ 16 chars
llmGeminiApiKey https://aistudio.google.com/
llmOpenrouterApiKey https://openrouter.ai/
braveApiKey https://api.search.brave.com/
serperApiKey https://serper.dev/
resendApiKey https://resend.com/

Without llmGeminiApiKey (and llmOpenrouterApiKey), free-form chat replies degrade to a templated provider: "echo" message and no natural-language tool dispatch happens — slash commands still work. Without braveApiKey and serperApiKey, /search uses the keyless Wikipedia OpenSearch fallback. Without resendApiKey, /email returns a clear configuration error.

Non-secret config

The agent's typed Config<AppConfig> has sensible Rust-side defaults for every field (model names, timezone, poll intervals, etc.), so no manifest overrides are needed for a working deployment. When you do want to override something, declare it under agents.<Name>.config (or component-level presets) using snake_case keys that match the Rust field names verbatim:

agents:
  LlmRouterAgent:
    config:
      llm_openrouter_function_calling_models:
        - google/gemma-4-31b-it:free

For integration tests, the harness pre-creates TelegramGatewayAgent("main") with --config telegram_api_base_url="http://127.0.0.1:9099" so outbound messages hit the local mock Telegram server.

For polling behavior, local mode sets:

components:
  golemclaw:rust-main:
    presets:
      debug:
        config:
          telegram_polling_enabled: true

If omitted, telegram_polling_enabled defaults to false.

Runtime fallback behavior

These are the fallback semantics implemented in code (not just config defaults):

  • LLM routing:
    • mode=auto: tries gemini, then openrouter (when those keys are configured).
    • no LLM key configured: returns local provider: echo template.
    • mode=auto provider failures: tries next provider; if all fail, replies "LLM unavailable".
    • explicit mode=gemini|openrouter: no cross-provider fallback on runtime failure.
  • OpenRouter model gates:
    • with llm_free_only=true (default), model must end with :free or be in llm_openrouter_allowed_free_models.
    • tool calling is only attached for models listed in llm_openrouter_function_calling_models; other models still chat, but without tools.
  • Search provider selection:
    • picks provider by key presence priority (Brave > Serper > Wikipedia).
    • this is selection-time fallback, not runtime failover: a Brave request error does not auto-retry with Serper.
  • Location/timezone fallback chain:
    • on shared location success: timezone source is Open-Meteocountry->IANA tablelongitude->Etc/GMT*.
    • if reverse-geocoding fails entirely: bot still stores lat/lon, infers timezone from longitude, and tells user it is approximate.
    • if stored timezone is invalid/missing, scheduling/reminder parsing falls back to UTC.
  • Telegram transport fallbacks:
    • missing telegramWebhookSecret: webhook stays open (unauthenticated) and logs a warning on each webhook call.
    • setMyCommands registration is best-effort and retried on later webhook/poll cycles.
    • callback-query ACK (answerCallbackQuery) is best-effort; failures degrade UX only.
    • poller mode with missing bot token waits and retries with backoff.
  • List pagination fallback:
    • out-of-range page callbacks are clamped to the nearest valid page instead of failing.

Tests and CI

Unit tests

179 host tests in cargo test cover the pure modules: time_utils (incl. IANA-timezone-aware parsing and bare-integer rejection), commands (incl. /timezone, /cancel_event, per-item Cancel callback parsing, mark_done/done action aliasing), domain (incl. tool-call validation for every catalog entry — cancel_event and commitment_action among them — paginate clamping, country → IANA table, longitude → Etc/GMT* fallback, self-reference detection, forget_fact exact-key removal), messages (incl. bot-command catalogue limits and /cancel_event listing), tool_clients (incl. HTML escaping of search results and country-suffix stripping in any language), tool_catalog (memory_tools_are_free_tier for both remember_fact / forget_fact, fetch_tools_feed_back_to_llm / action_tools_do_not_feed_back_to_llm for the agentic-loop classification, side_effecting_tools_require_a_capability extended to cover cancel_event and commitment_action), telegram_html (HTML escape + builders), llm_router_agent's function-declaration builders, Gemini and OpenRouter multi-turn message-shape builders (gemini_contents_for_request, openrouter_messages_for_request emit the correct functionCall / functionResponse / tool_calls / tool role sequences when prior_steps is populated), and user_brain_agent's synthesize_memory_ack helper (learn-only / forget-only / mixed / timezone tail notes).

Local end-to-end harness

bash tests/integration/e2e_local.sh

This script:

  1. Starts a mock Telegram API server (Python stdlib) on 127.0.0.1:9099 that records every outbound sendMessage to JSONL.
  2. Starts an isolated local Golem server on the standard local ports (9881 / 9006 / 9007) with an isolated data directory.
  3. Deploys against the test environment, then pre-creates TelegramGatewayAgent("main") with telegram_api_base_url: http://127.0.0.1:9099 so the bot's outbound traffic goes to the mock.
  4. POSTs every fixture in tests/integration/fixtures/ to the webhook.
  5. Asserts the mock observed the expected replies (e.g. "Weather in", "Top results for", "LLM mode", echo fallback message, conflict detected, reminder delivery).
  6. Waits ~12 s for a scheduled reminder to fire and verifies delivery.
  7. Probes MCP tools/list for a non-empty catalogue.

Zero real Telegram, zero third-party keys.

CI (GitHub Actions)

.github/workflows/ci.yml runs on every push, pull request, manual dispatch, and tag. It:

  1. Installs golem and golem-cli from GitHub Releases (configurable to latest, master, or 1.5.1 via workflow inputs).
  2. Runs cargo fmt --check, cargo clippy --target wasm32-wasip2 -- -Dwarnings, cargo test, cargo check --target wasm32-wasip2, and golem-cli build.
  3. Runs bash tests/integration/e2e_local.sh.
  4. Uploads the harness artifacts on failure.

Golem 1.5 features in use

  • #[agent_definition] with mount = "/..." HTTP routes; periodic snapshotting is intentionally not enabled because some agent fields (Config<AppConfig>, QuotaToken) are not Serialize / DeserializeOwned. Durable replay via the oplog is used instead and remains fast for the workloads in this build.
  • #[endpoint(get/post/...)] HTTP routes; webhook ingress with header-mapped secret.
  • #[agent_config] typed config (Config<AppConfig>) with Secret<String> fields.
  • schedule_* self-invocations for reminders, commitment nudges, due/escalation checks, outbox polling.
  • golem_rust::generate_idempotency_key() for replay-safe IDs.
  • atomically_async around the Resend HTTP round-trip.
  • golem_rust::bindings::golem::api::context::start_span for custom application spans on each tool branch (ready for OTLP — the golem-otlp-exporter plugin itself is not currently installed in any preset because the 1.5.0 build traps on durable jumps; install it manually with plugin install once a fixed version ships).
  • QuotaToken per user (LLM, search, weather, email) with resourceDefaults per environment.
  • retryPolicyDefaults for HTTP 5xx and 429 across local, test, and cloud.
  • httpApi mounts (Telegram webhook on every environment, fixed cloud domain from golem.yaml).
  • mcp deployment exposing agents as MCP tools.
  • Dedicated test environment with componentPresets: test, an isolated server harness, and a mock Telegram backend.

About

Telegram AI assistant with durable multi-agent backend, reminders, web search, scheduling & more. Built on Golem.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors