Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 18 additions & 37 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ A Telegram interface to an always-on personal assistant agent running on [exe.de
│ │ ├── command.ts # register Telegram bot commands (/start, /stop, /new, /models, /model <name>)
│ │ ├── queue.ts # serialize dispatch per session, buffering messages sent while one is in flight
│ │ ├── reply.ts # manage streaming reply previews and final response chunks
│ │ └── session.ts # build the flue session key from Telegram chat and thread IDs
│ │ └── session.ts # build the flue session key, manage session model state
│ ├── app.ts # HTTP entrypoint: mounts flue(), starts the grammY bot
│ ├── config.ts # app configuration from environment variables
│ ├── db.ts # discovered Flue persistence adapter (sqlite)
│ ├── log.ts # tslog logger
│ ├── markdown.ts # convert model output to Telegram Markdown v2
│ └── model.ts # fetches/caches the live model list and per-session model state
│ └── model.ts # fetches/caches the live model list
├── systemd
│ └── exe-agent.service # systemd unit file
└── test
Expand All @@ -34,24 +34,6 @@ A Telegram interface to an always-on personal assistant agent running on [exe.de
└── *.test.ts # unit tests
```

## Architecture

```
Telegram ⟷ grammY long-poll (started from src/app.ts)
dispatch(agent, { id, input })
Flue Node.js server
├─ src/agents/assistant.ts — defineAgent(...): system prompt, model, cwd
├─ src/model.ts — fetches/caches models, holds per-session overrides, composes `exe/<provider>/<name>` spec
└─ src/db.ts — flue's built-in SQLite session/execution store
pi-ai (flue dependency) ⟷ exe.dev LLM gateway (openai-completions API)
```

- **Inbound**: grammY `bot.on('message:text')` → allowlist check against `TELEGRAM_ALLOWED_USER` → `queue.submit(key, target, text)` (`src/telegram/queue.ts`).
* Per session, at most one dispatch is in flight at a time. If a message arrives while one is running, its text is buffered instead of starting a new placeholder/dispatch; when the in-flight turn settles (`agent_end` or error), any buffered messages are joined with `\n\n` and dispatched together as a single follow-up turn with its own placeholder. `/new` clears the buffer via `queue.reset(key)`.
- **Session key**: `telegram:v1:regular:chat:<chatId>:thread:<threadId|''>:direct:` (`src/telegram/session.ts`).
Expand All @@ -63,17 +45,17 @@ Telegram ⟷ grammY long-poll (started from src/app.ts)
* Model ids with "embedding" in them are dropped, and provider-less ids (e.g. `gpt-5.6-sol`) are deduped in favor of their provider-qualified counterpart (e.g. `openai/gpt-5.6-sol`), so `/model` and `/models` only ever deal in bare names.
* `EXE_AGENT_ALLOWED_MODELS` (comma-separated; optional) restricts and orders which fetched models are exposed; `initModels` throws at startup if a listed name isn't in the fetched set. Unset means every fetched model is available, in API order. The first name is the default.
* Per-session overrides live in an in-memory `Map` (not persisted across restarts).
* Resolution order (`src/model.ts`'s `getSessionModel`/`getDefaultModelName`, called from `src/agents/assistant.ts`'s `defineAgent`): session override → `EXE_AGENT_DEFAULT_MODEL` → first entry in the `allowedModels` list.
* Resolution order (`src/telegram/session.ts`'s `getSessionModel` and `src/model.ts`'s `getDefaultModelName`, called from `src/agents/assistant.ts`'s `defineAgent`): session override → `EXE_AGENT_DEFAULT_MODEL` → first entry in the `allowedModels` list.
* `/model <name>` replies with an "Unknown model" message (and points to `/models`) rather than erroring when the name isn't available.
* Names are composed into the exe.dev spec as `<provider>/<name>` (see `composeModelSpecifier()`), using the provider recorded for that model at fetch time.
* `apiKey` ("none"), `contextWindow`, and `maxTokens` must be set when registering the `exe` provider (`src/agents/assistant.ts`).
- **Commands**:
* `/start` - greeting
* `/stop` - stop the in-flight turn: invalidate any not-yet-admitted work, stop the reply preview, and `POST /agents/assistant/<key>/abort` in-process via `app.request(...)`; replies "Stopped." / "Nothing is currently running." / error
* `/new` - abort in-flight turn (`POST /agents/assistant/<key>/abort` called in-process via `app.request(...)`), clear session model override, fresh session
* `/new` - abort in-flight turn (`POST /agents/assistant/<key>/abort` called in-process via `app.request(...)`), delete session model override, fresh session
* `/models` - list available models, marking active/default
* `/model <name>` - switch model for this session
- **Manual/other-client access**: `src/agents/assistant.ts` exports a no-op `route: AgentRouteHandler`; the `POST /agents/assistant/:id` and `GET /agents/assistant/:id` (event streaming) endpoints are auto-generated by flue from the filename. The `route` export is a hook to insert middleware on those routes (currently unused).
- **Manual/other-client access**: `src/agents/assistant.ts` exports a no-op `route: AgentRouteHandler`; the `POST /agents/assistant/:id` and `GET /agents/assistant/:id` (event streaming) endpoints are auto-generated by flue from the filename.
- **No webhook**: long-polling only.
* Outbound-only connections from the VM, no public HTTPS route required.

Expand Down Expand Up @@ -110,9 +92,9 @@ Use `npm run test` to run tests with `node --test` using `node:test` and `node:a

## Running

Development: `npm run dev` (`flue dev`, reloads on change).
Development: `npm run dev` (`vite dev`, reloads on change).

Production: `npm run build` (`flue build`, emits `dist/server.mjs`) then `npm start` (`node --env-file-if-exists=.env dist/server.mjs`). The built server itself does not load `.env`; the `--env-file-if-exists` Node flag handles that here.
Production: `npm run build` (`vite build`, emits `dist/server.mjs`) then `npm start` (`node --env-file-if-exists=.env dist/server.mjs`). The built server itself does not load `.env`; the `--env-file-if-exists` Node flag handles that here.

The user will run the dev or start command in a separate terminal tab and verify functionality in Telegram.

Expand All @@ -121,11 +103,10 @@ Required environment variables:
* `TELEGRAM_ALLOWED_USER`

Optional:
* `EXE_AGENT_ALLOWED_MODELS` - comma-separated bare model names to expose, e.g. `deepseek-v4-flash,glm-5p2,kimi-k3` (defaults to `GET https://llm.int.exe.xyz/v1/models`)
* `EXE_AGENT_ALLOWED_MODELS` - comma-separated bare model names to expose, e.g. `deepseek-v4-flash,gpt-5.6-luna,grok-4.5` (defaults to `GET https://llm.int.exe.xyz/v1/models`)
* `EXE_AGENT_DEFAULT_MODEL` - default model override, falls back to the first entry in `EXE_AGENT_ALLOWED_MODELS`
* `EXE_AGENT_CONTEXT_WINDOW` - context window size in tokens passed to `registerProvider` (defaults to `256000`)
* `EXE_AGENT_MAX_TOKENS` - max tokens per response passed to `registerProvider` (defaults to `128000`)
* `EXE_AGENT_PROCESS_ENV` - set to `1` or `true` to pass `process.env` to the agent (defaults to `false`)
* `EXE_AGENT_CONTEXT_WINDOW` - context window size in tokens passed to `setProvider` (defaults to `256000`)
* `EXE_AGENT_MAX_TOKENS` - max tokens per response passed to `setProvider` (defaults to `64000`)
* `EXE_AGENT_HOME` - default current working directory for the agent
* `LOG_LEVEL` - defaults to `"info"`

Expand All @@ -138,13 +119,13 @@ Via `systemd/exe-agent.service` with `Restart=always` as user:group `exedev:exed
## References

Pinned to the versions in `package.json`:
- flue (`v1.0.0-beta.9`) — agent/routing/runtime API used by `src/agents/assistant.ts`, `src/app.ts`, and `src/db.ts`, and the `@flue/cli` build/dev toolchain:
* [`packages/runtime/src/agent-definition.ts`](https://raw.githubusercontent.com/withastro/flue/v1.0.0-beta.9/packages/runtime/src/agent-definition.ts) — `defineAgent`
* [`packages/runtime/src/index.ts`](https://raw.githubusercontent.com/withastro/flue/v1.0.0-beta.9/packages/runtime/src/index.ts) — `dispatch`, `observe`, `registerProvider`
* [`packages/runtime/src/routing.ts`](https://raw.githubusercontent.com/withastro/flue/v1.0.0-beta.9/packages/runtime/src/routing.ts) — `flue()` Hono sub-app mounted from `src/app.ts`
* [`packages/runtime/src/node/index.ts`](https://raw.githubusercontent.com/withastro/flue/v1.0.0-beta.9/packages/runtime/src/node/index.ts) — `sqlite(path)` used by `src/db.ts`
* [`packages/runtime/src/client.ts`](https://raw.githubusercontent.com/withastro/flue/v1.0.0-beta.9/packages/runtime/src/client.ts)
- pi (`v0.80.10`) — the OpenAI-completions-compatible client flue uses to reach the exe.dev gateway:
* [`packages/ai/src/api/openai-completions.ts`](https://raw.githubusercontent.com/earendil-works/pi/v0.80.10/packages/ai/src/api/openai-completions.ts)
- flue (`v2.0.3`) — agent/routing/runtime API used by `src/agents/assistant.ts`, `src/app.ts`, and `src/db.ts`, and the `@flue/vite` build/dev toolchain:
* [`packages/runtime/src/agent-definition.ts`](https://raw.githubusercontent.com/withastro/flue/v2.0.3/packages/runtime/src/agent-definition.ts) — `defineAgent`
* [`packages/runtime/src/index.ts`](https://raw.githubusercontent.com/withastro/flue/v2.0.3/packages/runtime/src/index.ts) — `dispatch`, `observe`, `setProvider`
* [`packages/runtime/src/routing.ts`](https://raw.githubusercontent.com/withastro/flue/v2.0.3/packages/runtime/src/routing.ts) — `createAgentRouter()` Hono sub-app mounted from `src/app.ts`
* [`packages/runtime/src/node/index.ts`](https://raw.githubusercontent.com/withastro/flue/v2.0.3/packages/runtime/src/node/index.ts) — `sqlite(path)` used by `src/db.ts`
* [`packages/runtime/src/client.ts`](https://raw.githubusercontent.com/withastro/flue/v2.0.3/packages/runtime/src/client.ts)
- pi (`v0.83.0`) — the OpenAI-completions-compatible client flue uses to reach the exe.dev gateway:
* [`packages/ai/src/api/openai-completions.ts`](https://raw.githubusercontent.com/earendil-works/pi/v0.83.0/packages/ai/src/api/openai-completions.ts)
- grammY (`v1.45.1`) — Telegram bot framework, long-polling:
* [`src/bot.ts`](https://raw.githubusercontent.com/grammyjs/grammY/v1.45.1/src/bot.ts) — `Bot`, `.start()`, `handlePollingError`, 409 semantics
25 changes: 4 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@ A Telegram interface to an always-on personal AI assistant, running on [exe.dev]

I've been using exe.dev for remote development over SSH in VS Code and Zed and wanted to build a Telegram agent specifically for their platform.

I started with [OpenClaw](https://github.com/openclaw/openclaw) in early 2026. It is easy to deploy and works out of the box. It works best on a desktop like a Mac Mini with a residential IP address, not a cloud VM. It feels overkill for just a Telegram chat bot. I never used the web UI or CLI, and never used it as a coding agent. I didn't need to use the [browser](https://docs.openclaw.ai/cli/browser) tool to make restaurant reservations for me. I didn't need [heartbeats](https://docs.openclaw.ai/gateway/heartbeat) checking my email every hour, because I only check it once a week. I didn't find much value in [memory](https://docs.openclaw.ai/cli/memory).

I then tried [Hermes](https://github.com/nousresearch/hermes-agent), but I felt it was designed to be run with a [Nous Portal](https://portal.nousresearch.com) subscription and deployed to [Nous Cloud](https://portal.nousresearch.com/cloud). I did not find it superior to OpenClaw, though I understand that influencers need shiny new things to make videos about. That said, the Portal subscription is truly convenient in that you get LLMs, [Browser Use](https://browser-use.com), [Firecrawl](https://www.firecrawl.dev), [OpenAI](https://developers.openai.com/api/docs/guides/text-to-speech) TTS, [Fal](https://fal.ai) image/video generation, and [Modal](https://modal.com/products/sandboxes) sandboxes in a single API.

The exe.dev team maintains their own coding agent, [Shelley](https://github.com/boldsoftware/shelley), which you can prompt from the website or mobile app. I prefer it for remote system administration, not as a personal assistant.

I like [Owain Lewis](https://github.com/owainlewis)'s [Push](https://github.com/owainlewis/push). It sends tasks to your existing coding agents from Telegram, Slack, or iMessage. Scheduled tasks are just Markdown files in a `jobs` folder with TOML frontmatter. Durable information goes in a `context` folder. Evals explaining what "good" looks like go in `evals`.

I also like [PewDiePie](https://github.com/pewdiepie-archdaemon)'s [Odysseus](https://github.com/odysseus-dev/odysseus), which is more of an agentic workspace than a personal assistant.

## What is exe.dev

exe.dev is a new cloud from the [co-founder](https://github.com/crawshaw) of [Tailscale](https://tailscale.com). Instead of getting a single VM, you get a pool of compute, memory, and disk that you can use to create many [cloud-hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) VMs. Every VM has access to a LLM gateway with a monthly allocation of credits. They also include useful features like a reverse [proxy](https://exe.dev/docs/proxy) for web apps, [authentication](https://exe.dev/docs/login-with-exe) for users, bearer [tokens](https://exe.dev/docs/https-tokens-for-vms) for services, and [sending](https://exe.dev/docs/send-email)/[receiving](https://exe.dev/docs/receive-email) email.
Expand All @@ -24,10 +14,6 @@ exe.dev is a new cloud from the [co-founder](https://github.com/crawshaw) of [Ta

`flue` is a framework for building agents from the [Astro](https://github.com/withastro/astro) team at Cloudflare. It uses [`pi-ai`](https://github.com/earendil-works/pi/tree/main/packages/ai) and [`pi-agent-core`](https://github.com/earendil-works/pi/tree/main/packages/agent) under the hood. It can be deployed to Cloudflare workers, run as a [Hono](https://github.com/honojs/hono) server, or be embedded as a runtime. It provides automatic compaction, SQLite session persistence, and a durable job queue with lease-based ownership, so a crashed turn can be retried when the service is back online. The official Telegram channel only supports webhooks, so [`grammy`](https://github.com/grammyjs/grammy) is used for [long-polling](https://grammy.dev/guide/deployment-types#how-does-long-polling-work) instead of exposing a port.

I considered [`mastra`](https://github.com/mastra-ai/mastra), but it felt more like [LangChain](https://github.com/langchain-ai/langchain) in that it was for composing agentic workflows, not building personal assistants or coding agents.

I also considered [`eve`](https://github.com/vercel/eve), which gives you a lot out of the box, but seems designed for Slack agents (similar to Claude [Tag](https://www.anthropic.com/news/introducing-claude-tag)) deployed to Vercel using their AI Gateway.

## How it works

- **Inbound**: a text message is allow-listed against `TELEGRAM_ALLOWED_USER`, then queued per-session. At most one turn runs at a time per chat; anything that arrives mid-turn is buffered and joined into a single follow-up turn once the current one settles, rather than piling up concurrent dispatches or dropping messages.
Expand Down Expand Up @@ -70,8 +56,7 @@ Production:
npm run build
sudo cp ~/.exe-agent/systemd/exe-agent.service /etc/systemd/system/exe-agent.service
sudo systemctl daemon-reload
sudo systemctl enable exe-agent.service
sudo systemctl start exe-agent.service
sudo systemctl enable --now exe-agent.service
journalctl -u exe-agent -f # tail logs
```

Expand All @@ -90,8 +75,7 @@ Optional:
- `EXE_AGENT_ALLOWED_MODELS`: comma-separated model names to expose (defaults to `GET /v1/models`)
- `EXE_AGENT_DEFAULT_MODEL`: default model (defaults to `allowedModels[0]`)
- `EXE_AGENT_CONTEXT_WINDOW`: context window size in tokens (defaults to `256000`)
- `EXE_AGENT_MAX_TOKENS`: max tokens per response (defaults to `128000`)
- `EXE_AGENT_PROCESS_ENV`: provide `process.env` to the agent (defaults to `false`)
- `EXE_AGENT_MAX_TOKENS`: max tokens per response (defaults to `64000`)
- `EXE_AGENT_HOME`: working directory for the agent (defaults to `$HOME`)
- `LOG_LEVEL`: `INFO`, `ERROR`, or `FATAL` to disable (defaults to `INFO`)

Expand All @@ -106,9 +90,8 @@ TELEGRAM_ALLOWED_USER=<your-user-id>

EXE_AGENT_ALLOWED_MODELS=deepseek-v4-flash,glm-5p2,kimi-k3,minimax-m3
EXE_AGENT_DEFAULT_MODEL=minimax-m3
EXE_AGENT_CONTEXT_WINDOW=256000
EXE_AGENT_MAX_TOKENS=128000
EXE_AGENT_PROCESS_ENV=true
EXE_AGENT_CONTEXT_WINDOW=128000
EXE_AGENT_MAX_TOKENS=32000
EXE_AGENT_HOME=/home/exedev

LOG_LEVEL=info
Expand Down
5 changes: 0 additions & 5 deletions flue.config.ts

This file was deleted.

Loading