feat: auto router (#15)

This commit is contained in:
banteg
2026-01-02 03:13:29 +04:00
committed by GitHub
parent 73ba4836c1
commit bd9387f7f0
12 changed files with 694 additions and 73 deletions
+18 -9
View File
@@ -26,6 +26,9 @@ uv run ty check .
make check
```
Takopi runs in **auto-router** mode by default. `default_engine` in `takopi.toml` selects
the engine for new threads; engine subcommands override that default for the process.
## Module Responsibilities
### `bridge.py` - Telegram bridge loop
@@ -44,9 +47,11 @@ The orchestrator module containing:
**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)
- Progress edits are throttled to ~1s intervals and only run when new events arrive
- `/{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>` ``)
- Resume parsing is delegated to the active runner (no cross-engine fallback)
- 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)
### `cli.py` - CLI entry point
@@ -164,12 +169,16 @@ poll_updates() drains backlog, long-polls, filters chat_id == from_id == cfg.cha
run_main_loop() spawns tasks in TaskGroup
handle_message() spawned as task
router.resolve_resume(text, reply_text) → ResumeToken | None
router.runner_for(resume_token) → selects runner (default engine if None)
handle_message() spawned as task with selected runner
Send initial progress message (silent)
CodexRunner.run()
├── Spawns: codex exec --json ... -
runner.run(prompt, resume_token)
├── Spawns engine subprocess (e.g., codex exec --json)
├── Streams JSONL from stdout
├── Normalizes JSONL -> takopi events
├── Yields Takopi events (async iterator)
@@ -186,10 +195,10 @@ Send/edit final message
### Resume Flow
Same as above, but:
- Runners parse resume lines (e.g. `` `codex resume <token>` ``)
- Command becomes: `codex exec --json resume <token> -`
- Per-token lock serializes concurrent resumes
Same as above; auto-router polls all runners to extract resume tokens:
- Router returns first matching token (e.g. `` `claude --resume <id>` `` routes to Claude)
- Selected runner spawns with resume (e.g. `codex exec --json resume <token> -`)
- Per-token lock serializes concurrent resumes on the same thread
## Error Handling
+51 -13
View File
@@ -1,10 +1,10 @@
# Takopi Specification v0.2.0 (minimal) [2025-12-31]
# Takopi Specification v0.4.0 [2026-01-01]
This document is **normative**. The words **MUST**, **SHOULD**, and **MAY** express requirements.
## 1. Scope
Takopi v0.2.0 specifies:
Takopi v0.4.0 specifies:
- A **Telegram** bot bridge that runs an agent **Runner** and posts:
- a throttled, edited **progress message**
@@ -12,12 +12,12 @@ Takopi v0.2.0 specifies:
- **Thread continuation** via a **resume command** embedded in chat messages
- **Parallel runs across different threads**
- **Serialization within a thread** (no concurrent runs on the same thread)
- **Automatic runner selection** among multiple engines based on ResumeLine (with a configurable default for new threads)
- A Takopi-owned **normalized event model** produced by runners and consumed by renderers/bridge
Out of scope for v0.2.0:
Out of scope for v0.4.0:
- Non-Telegram clients (Slack/Discord/etc.)
- Auto-selecting among multiple runners
- Token-by-token streaming of the assistants final answer
- Engines/runners that cannot provide **stable action IDs** within a run
@@ -71,11 +71,13 @@ Constraints:
### 3.4 Bridge resume resolution (MUST)
Given `text` (user message) and optional `reply_text` (the message being replied to):
Given `text` (user message), optional `reply_text` (the message being replied to), and an ordered list of available runners `runners`:
1. The bridge MUST attempt `runner.extract_resume(text)`.
2. If not found, it MUST attempt `runner.extract_resume(reply_text)` if present.
3. If still not found, the run MUST start with `resume=None` (new thread).
1. The bridge MUST attempt to extract a resume token by polling all runners in order:
1. for each `r` in `runners`, attempt `r.extract_resume(text)`
2. choose the **first** runner that returns a non-`None` token and stop
2. If not found, it MUST repeat step (1) for `reply_text` if present.
3. If still not found, the run MUST start with `resume=None` (new thread) on the default runner (per §8, including chat-level overrides).
## 4. Normalized event model
@@ -335,12 +337,30 @@ Action update collapsing:
## 8. Configuration and engine selection
Decision (v0.2.0):
Decision (v0.4.0):
* Exactly one runner is selected at startup via a CLI subcommand (no default).
* If no engine subcommand is provided, Takopi prints an engine chooser panel and exits.
* Resume extraction uses only the selected runner.
* If a user provides a resume line for a different engine, extraction fails and the bridge treats the message as a new thread (`resume=None`).
* Takopi MUST support configuring a **default engine** used to start new threads (`resume=None`).
* If not configured, the default engine is implementation-defined (non-normative: the reference implementation defaults to `codex`).
* If no engine subcommand is provided, Takopi MUST run in **auto-router** mode:
* new threads use the configured default engine
* resumed threads are routed based on ResumeLine extraction (per §3.4)
* If an engine subcommand is provided, Takopi MUST still use the auto-router, but it overrides the configured default engine for new threads.
* Resume extraction MUST poll **all** available runners (per §3.4) and route to the first matching runner.
* New thread engine override (chat-level):
* Users MAY prefix the first non-empty line with `/{engine}` (e.g. `/claude` or `/codex`) to select the engine for a **new** thread.
* The bridge MUST strip that directive from the prompt before invoking the runner.
* If a ResumeToken is resolved from the message or reply, it MUST take precedence and the `/{engine}` directive MUST be ignored.
### 8.1 Command menu (Telegram)
Takopi SHOULD keep the bots slash-command menu in sync at startup by calling
`setMyCommands` with the canonical list of supported commands.
* The command list MUST include:
* `cancel` — cancel the current run
* one entry per configured engine
* The command list MUST NOT include commands the bot does not support.
* Command descriptions SHOULD be terse and lowercase.
## 9. Testing requirements (MUST)
@@ -373,5 +393,23 @@ Tests MUST cover:
* completed-only actions render correctly
* repeated events for same Action.id collapse as intended
7. **Auto-router engine selection**
* resume lines for non-default engines are detected and routed correctly (poll all runners)
* new threads use the configured default engine, with CLI subcommand overriding it
Test tooling SHOULD include event factories, deterministic/fake time, and a script/mock runner.
## 10. Changelog
### v0.4.0 (2026-01-01)
- Add auto-router engine selection by polling all runners to decode resume lines; add configurable default engine for new threads (subcommand overrides default).
### v0.3.0 (2026-01-01)
- Require runners to implement explicit resume formatting/extraction/detection and treat runners as authoritative for resume tokens/lines.
### v0.2.0 (2025-12-31)
- Initial minimal Takopi specification (Telegram bridge + runner protocol + normalized events + resume support).