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.
- Highlights
- Example session
- Architecture
- Tools
- Build and run locally
- Deploy to Golem Cloud
- Configuration
- Tests and CI
- Golem 1.5 features in use
- 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, OpenRoutertools). 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 asfunctionResponse/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 validtimezonefact 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 callsforget_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/timezoneas 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:00are anchored to the user's IANA timezone (Europe/Rome,America/New_York, …) instead of UTC. Backed bychrono-tzwith a layered fallback (Open-Meteo → country table → longitude approximation) so the bot always produces a usable zone. Override with/timezone Europe/Romeat 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
@handlementions). Long/reminders//events//commitmentslists 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 viasetMyCommandsso 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: echotemplate. 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 singletonSharedMemoryAgent()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, explicitsnapshotting = "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.
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.
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.
| 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 |
| 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 |
(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 |
/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.
Prerequisites:
- Rust toolchain with the
wasm32-wasip2target golemandgolem-cli1.5.x inPATHcurl,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 --yesLocal default URLs:
- Telegram webhook:
http://golemclaw.localhost:9006/telegram/main/webhook - MCP:
http://golemclaw.localhost:9007/mcp
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")' startIn 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.
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.
-
A Golem Cloud account and a
golem-cliprofile 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 currentandgolem-cli account list. -
A Telegram bot. Talk to
@BotFatherin Telegram, run/newbot, pick a name and username, and save the bot token.
# 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}\"
}"# 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>.
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.
| 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.
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:freeFor 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: trueIf omitted, telegram_polling_enabled defaults to false.
These are the fallback semantics implemented in code (not just config defaults):
- LLM routing:
mode=auto: triesgemini, thenopenrouter(when those keys are configured).- no LLM key configured: returns local
provider: echotemplate. mode=autoprovider 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:freeor be inllm_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.
- with
- 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-Meteo→country->IANA table→longitude->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.
- on shared location success: timezone source is
- Telegram transport fallbacks:
- missing
telegramWebhookSecret: webhook stays open (unauthenticated) and logs a warning on each webhook call. setMyCommandsregistration 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.
- missing
- List pagination fallback:
- out-of-range page callbacks are clamped to the nearest valid page instead of failing.
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).
bash tests/integration/e2e_local.shThis script:
- Starts a mock Telegram API server (Python stdlib) on
127.0.0.1:9099that records every outboundsendMessageto JSONL. - Starts an isolated local Golem server on the standard local ports
(
9881/9006/9007) with an isolated data directory. - Deploys against the
testenvironment, then pre-createsTelegramGatewayAgent("main")withtelegram_api_base_url: http://127.0.0.1:9099so the bot's outbound traffic goes to the mock. - POSTs every fixture in
tests/integration/fixtures/to the webhook. - Asserts the mock observed the expected replies (e.g. "Weather in", "Top results for", "LLM mode", echo fallback message, conflict detected, reminder delivery).
- Waits ~12 s for a scheduled reminder to fire and verifies delivery.
- Probes MCP
tools/listfor a non-empty catalogue.
Zero real Telegram, zero third-party keys.
.github/workflows/ci.yml runs on every push, pull request, manual
dispatch, and tag. It:
- Installs
golemandgolem-clifrom GitHub Releases (configurable tolatest,master, or1.5.1via workflow inputs). - Runs
cargo fmt --check,cargo clippy --target wasm32-wasip2 -- -Dwarnings,cargo test,cargo check --target wasm32-wasip2, andgolem-cli build. - Runs
bash tests/integration/e2e_local.sh. - Uploads the harness artifacts on failure.
#[agent_definition]withmount = "/..."HTTP routes; periodic snapshotting is intentionally not enabled because some agent fields (Config<AppConfig>,QuotaToken) are notSerialize/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>) withSecret<String>fields.schedule_*self-invocations for reminders, commitment nudges, due/escalation checks, outbox polling.golem_rust::generate_idempotency_key()for replay-safe IDs.atomically_asyncaround the Resend HTTP round-trip.golem_rust::bindings::golem::api::context::start_spanfor custom application spans on each tool branch (ready for OTLP — thegolem-otlp-exporterplugin itself is not currently installed in any preset because the 1.5.0 build traps on durable jumps; install it manually withplugin installonce a fixed version ships).QuotaTokenper user (LLM, search, weather, email) withresourceDefaultsper environment.retryPolicyDefaultsfor HTTP 5xx and 429 acrosslocal,test, andcloud.httpApimounts (Telegram webhook on every environment, fixed cloud domain fromgolem.yaml).mcpdeployment exposing agents as MCP tools.- Dedicated
testenvironment withcomponentPresets: test, an isolated server harness, and a mock Telegram backend.