refactor!: split telegram bridge into transport/presenter (#55)
This commit is contained in:
+68
-23
@@ -23,7 +23,7 @@ uv run ruff check src tests
|
||||
uv run ty check .
|
||||
|
||||
# Or all at once
|
||||
make check
|
||||
just check
|
||||
```
|
||||
|
||||
Takopi runs in **auto-router** mode by default. `default_engine` in `takopi.toml` selects
|
||||
@@ -31,47 +31,86 @@ the engine for new threads; engine subcommands override that default for the pro
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
### `bridge.py` - Telegram bridge loop
|
||||
### `runner_bridge.py` - Transport-agnostic orchestration
|
||||
|
||||
The orchestrator module containing:
|
||||
The core handler module containing:
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `BridgeConfig` | Frozen dataclass holding runtime config |
|
||||
| `poll_updates()` | Async generator that drains backlog, long-polls updates, filters messages |
|
||||
| `run_main_loop()` | TaskGroup-based main loop that spawns per-message handlers |
|
||||
| `ExecBridgeConfig` | Frozen dataclass holding transport + presenter config |
|
||||
| `IncomingMessage` | Normalized incoming message shape |
|
||||
| `handle_message()` | Per-message handler with progress updates and final render |
|
||||
| `ProgressEdits` | Throttled progress edit worker |
|
||||
| `RunningTask` | Cancellation + resume coordination for in-flight runs |
|
||||
|
||||
**Key patterns:**
|
||||
- Progress edits are best-effort and only run when new events arrive (Telegram outbox handles rate limiting/coalescing)
|
||||
- Resume tokens are runner-formatted command lines (e.g., `` `codex resume <token>` ``, `` `claude --resume <token>` ``, `` `pi --session <path>` ``)
|
||||
- Resume lines are stripped from the prompt before invoking the runner
|
||||
- Errors/cancellation render final status while preserving resume tokens when known
|
||||
|
||||
### `telegram/bridge.py` - Telegram bridge loop
|
||||
|
||||
The Telegram adapter module containing:
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `TelegramBridgeConfig` | Frozen dataclass holding bot + router + exec config |
|
||||
| `TelegramTransport` | `BotClient` → `Transport` adapter |
|
||||
| `TelegramPresenter` | `ProgressState` → `RenderedMessage` adapter |
|
||||
| `poll_updates()` | Async generator that drains backlog, long-polls updates, filters messages |
|
||||
| `run_main_loop()` | TaskGroup-based main loop that spawns per-message handlers |
|
||||
| `_handle_cancel()` | `/cancel` routing |
|
||||
|
||||
**Key patterns:**
|
||||
- Bridge schedules runs FIFO per thread to avoid concurrent progress messages; runner locks enforce per-thread serialization
|
||||
- `/cancel` routes by reply-to progress message id (accepts extra text)
|
||||
- `/{engine}` on the first line selects the engine for new threads
|
||||
- Progress edits are throttled to 2s intervals and only run when new events arrive
|
||||
- Resume tokens are runner-formatted command lines (e.g., `` `codex resume <token>` ``, `` `claude --resume <token>` ``, `` `pi --session <path>` ``)
|
||||
- Resume parsing polls all runners via `AutoRouter.resolve_resume()` and routes to the first match
|
||||
- Bot command menu is synced on startup (`cancel` + engine commands)
|
||||
|
||||
### `transport.py` - Transport protocol
|
||||
|
||||
Defines `Transport`, `MessageRef`, `RenderedMessage`, and `SendOptions`.
|
||||
|
||||
### `presenter.py` - Presenter protocol
|
||||
|
||||
Defines a renderer that converts `ProgressState` into `RenderedMessage` outputs.
|
||||
|
||||
### `cli.py` - CLI entry point
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `run()` / `main()` | Typer CLI entry points |
|
||||
| `_parse_bridge_config()` | Reads config + builds `BridgeConfig` |
|
||||
| `_parse_bridge_config()` | Reads config + builds `TelegramBridgeConfig` + `ExecBridgeConfig` |
|
||||
|
||||
### `render.py` - Takopi event + Markdown helpers
|
||||
### `progress.py` - Progress tracking
|
||||
|
||||
| Function/Class | Purpose |
|
||||
|----------------|---------|
|
||||
| `ProgressTracker` | Stateful reducer of takopi events into progress snapshots |
|
||||
| `ProgressState` | Snapshot of actions, resume token, and engine metadata |
|
||||
|
||||
### `markdown.py` - Markdown formatting
|
||||
|
||||
| Function/Class | Purpose |
|
||||
|----------------|---------|
|
||||
| `MarkdownFormatter` | Converts `ProgressState` into MarkdownParts |
|
||||
| `MarkdownPresenter` | `ProgressState` → `RenderedMessage` (markdown text) |
|
||||
| `MarkdownParts` | Header/body/footer building blocks for markdown output |
|
||||
| `assemble_markdown_parts()` | Join MarkdownParts into a single markdown string |
|
||||
| `render_event_cli()` | Format a takopi event for CLI logs |
|
||||
| `format_elapsed()` | Formats seconds as `Xh Ym`, `Xm Ys`, or `Xs` |
|
||||
|
||||
### `telegram/render.py` - Telegram markdown rendering
|
||||
|
||||
| Function/Class | Purpose |
|
||||
|----------------|---------|
|
||||
| `render_markdown()` | Markdown → Telegram text + entities |
|
||||
| `trim_body()` | Trim body to 3500 chars (header/footer preserved) |
|
||||
| `prepare_telegram()` | Trim + render Markdown parts for Telegram |
|
||||
| `ExecProgressRenderer` | Stateful renderer tracking recent actions for progress display |
|
||||
| `render_event_cli()` | Format a takopi event for CLI logs |
|
||||
| `format_elapsed()` | Formats seconds as `Xh Ym`, `Xm Ys`, or `Xs` |
|
||||
|
||||
### `telegram.py` - Telegram API wrapper
|
||||
### `telegram/client.py` - Telegram API wrapper
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
@@ -184,7 +223,13 @@ Self-documenting msgspec schemas for decoding engine JSONL streams.
|
||||
|----------|---------|
|
||||
| `install_issue()` | Creates `SetupIssue` with install instructions for missing CLI |
|
||||
|
||||
### `config.py` - Configuration loading
|
||||
### `config.py` - Shared configuration errors
|
||||
|
||||
```python
|
||||
class ConfigError(RuntimeError): ...
|
||||
```
|
||||
|
||||
### `telegram/config.py` - Configuration loading
|
||||
|
||||
```python
|
||||
def load_telegram_config() -> tuple[dict, Path]:
|
||||
@@ -210,7 +255,7 @@ Environment flags:
|
||||
|
||||
CLI flag: `--debug` enables debug logging (overrides `TAKOPI_LOG_LEVEL`).
|
||||
|
||||
### `onboarding.py` - Setup validation
|
||||
### `telegram/onboarding.py` - Setup validation
|
||||
|
||||
```python
|
||||
def check_setup(backend: EngineBackend) -> SetupResult:
|
||||
@@ -231,15 +276,15 @@ See `docs/adding-a-runner.md` for the full guide and a worked example.
|
||||
```
|
||||
Telegram Update
|
||||
↓
|
||||
poll_updates() drains backlog, long-polls, filters chat_id == cfg.chat_id
|
||||
telegram/bridge.poll_updates() drains backlog, long-polls, filters chat_id == cfg.chat_id
|
||||
↓
|
||||
run_main_loop() spawns tasks in TaskGroup
|
||||
telegram/bridge.run_main_loop() spawns tasks in TaskGroup
|
||||
↓
|
||||
router.resolve_resume(text, reply_text) → ResumeToken | None
|
||||
↓
|
||||
router.entry_for(resume_token) or router.entry_for_engine(default) → RunnerEntry
|
||||
router.entry_for(resume_token) or router.entry_for_engine(override/default) → RunnerEntry
|
||||
↓
|
||||
handle_message() spawned as task with selected runner
|
||||
runner_bridge.handle_message() spawned as task with selected runner
|
||||
↓
|
||||
Send initial progress message (silent)
|
||||
↓
|
||||
@@ -249,14 +294,14 @@ runner.run(prompt, resume_token)
|
||||
├── Normalizes JSONL -> takopi events
|
||||
├── Yields Takopi events (async iterator)
|
||||
│ ↓
|
||||
│ ExecProgressRenderer.note_event()
|
||||
│ ProgressTracker.note_event()
|
||||
│ ↓
|
||||
│ ProgressEdits throttled edit_message_text()
|
||||
│ ProgressEdits best-effort transport.edit(wait=False)
|
||||
└── Ends with completed(resume, ok, answer)
|
||||
↓
|
||||
render_final() with resume line (runner-formatted)
|
||||
↓
|
||||
Send/edit final message
|
||||
transport.send()/edit() final message, delete progress if needed
|
||||
```
|
||||
|
||||
### Resume Flow
|
||||
|
||||
Reference in New Issue
Block a user