263 lines
7.6 KiB
Markdown
263 lines
7.6 KiB
Markdown
# Plugin API
|
||
|
||
Takopi’s **public plugin API** is exported from:
|
||
|
||
```
|
||
takopi.api
|
||
```
|
||
|
||
Anything not imported from `takopi.api` should be considered **internal** and
|
||
subject to change. The API version is tracked by `TAKOPI_PLUGIN_API_VERSION`.
|
||
|
||
---
|
||
|
||
## Versioning
|
||
|
||
- Current API version: `TAKOPI_PLUGIN_API_VERSION = 1`
|
||
- Plugins should pin to a compatible Takopi range, e.g.:
|
||
|
||
```toml
|
||
dependencies = ["takopi>=0.14,<0.15"]
|
||
```
|
||
|
||
---
|
||
|
||
## Exported symbols
|
||
|
||
### Engine backends and runners
|
||
|
||
| Symbol | Purpose |
|
||
|--------|---------|
|
||
| `EngineBackend` | Declares an engine backend (id + runner builder) |
|
||
| `EngineConfig` | Dict-based engine config table |
|
||
| `Runner` | Runner protocol |
|
||
| `BaseRunner` | Helper base class with resume locking |
|
||
| `JsonlSubprocessRunner` | Helper for JSONL-streaming CLIs |
|
||
| `EventFactory` | Helper for building takopi events |
|
||
|
||
### Transport backends
|
||
|
||
| Symbol | Purpose |
|
||
|--------|---------|
|
||
| `TransportBackend` | Transport backend protocol |
|
||
| `SetupIssue` | Setup issue for onboarding / validation |
|
||
| `SetupResult` | Setup issues + config path |
|
||
| `Transport` | Transport protocol (send/edit/delete) |
|
||
| `Presenter` | Renders progress to `RenderedMessage` |
|
||
| `RenderedMessage` | Rendered text + transport metadata |
|
||
| `SendOptions` | Reply/notify/replace flags |
|
||
| `MessageRef` | Transport-specific message reference |
|
||
| `TransportRuntime` | Transport runtime facade (routers/projects hidden) |
|
||
| `ResolvedMessage` | Parsed prompt + resume/context resolution |
|
||
| `ResolvedRunner` | Runner selection result |
|
||
|
||
### Command backends
|
||
|
||
| Symbol | Purpose |
|
||
|--------|---------|
|
||
| `CommandBackend` | Slash command plugin protocol |
|
||
| `CommandContext` | Context passed to a command handler |
|
||
| `CommandExecutor` | Helper to send messages or run engines |
|
||
| `CommandResult` | Simple response payload for a command |
|
||
| `RunRequest` | Engine run request used by commands |
|
||
| `RunResult` | Engine run result (captured output) |
|
||
| `RunMode` | `"emit"` (send) or `"capture"` (collect) |
|
||
|
||
### Core types and helpers
|
||
|
||
| Symbol | Purpose |
|
||
|--------|---------|
|
||
| `EngineId` | Engine id type alias |
|
||
| `ResumeToken` | Resume token (engine + value) |
|
||
| `StartedEvent` / `ActionEvent` / `CompletedEvent` | Core event types |
|
||
| `Action` | Action metadata for `ActionEvent` |
|
||
| `RunContext` | Project/branch context |
|
||
| `ConfigError` | Configuration error type |
|
||
| `DirectiveError` | Error raised when parsing directives |
|
||
| `RunnerUnavailableError` | Router error when a runner is unavailable |
|
||
|
||
### Bridge helpers (for transport plugins)
|
||
|
||
| Symbol | Purpose |
|
||
|--------|---------|
|
||
| `ExecBridgeConfig` | Transport + presenter config |
|
||
| `IncomingMessage` | Normalized incoming message |
|
||
| `RunningTask` / `RunningTasks` | Per-message run coordination |
|
||
| `handle_message()` | Core message handler used by transports |
|
||
|
||
---
|
||
|
||
## Runner contract (engine plugins)
|
||
|
||
Runners emit events in a strict sequence (see `tests/test_runner_contract.py`):
|
||
|
||
- Exactly **one** `StartedEvent`
|
||
- Exactly **one** `CompletedEvent`
|
||
- `CompletedEvent` is **last**
|
||
- `CompletedEvent.resume == StartedEvent.resume`
|
||
|
||
Action events are optional. The minimal valid run is:
|
||
|
||
```
|
||
StartedEvent -> CompletedEvent
|
||
```
|
||
|
||
### Resume tokens
|
||
|
||
Runners own the resume format:
|
||
|
||
- `format_resume(token)` returns a command line users can paste
|
||
- `extract_resume(text)` parses resume tokens from user text
|
||
- `is_resume_line(line)` lets Takopi strip resume lines before running
|
||
|
||
---
|
||
|
||
## EngineBackend
|
||
|
||
```py
|
||
EngineBackend(
|
||
id: str,
|
||
build_runner: Callable[[EngineConfig, Path], Runner],
|
||
cli_cmd: str | None = None,
|
||
install_cmd: str | None = None,
|
||
)
|
||
```
|
||
|
||
- `id` must match the entrypoint name and the ID regex.
|
||
- `build_runner` should raise `ConfigError` for invalid config.
|
||
- `cli_cmd` is used to check whether the engine CLI is on `PATH`.
|
||
- `install_cmd` is surfaced in onboarding output.
|
||
|
||
---
|
||
|
||
## TransportBackend
|
||
|
||
```py
|
||
class TransportBackend(Protocol):
|
||
id: str
|
||
description: str
|
||
|
||
def check_setup(...) -> SetupResult: ...
|
||
def interactive_setup(self, *, force: bool) -> bool: ...
|
||
def lock_token(
|
||
self, *, transport_config: dict[str, object], config_path: Path
|
||
) -> str | None: ...
|
||
def build_and_run(
|
||
self,
|
||
*,
|
||
transport_config: dict[str, object],
|
||
config_path: Path,
|
||
runtime: TransportRuntime,
|
||
final_notify: bool,
|
||
default_engine_override: str | None,
|
||
) -> None: ...
|
||
```
|
||
|
||
Transport backends are responsible for:
|
||
|
||
- Validating config and onboarding users (`check_setup`, `interactive_setup`)
|
||
- Providing a lock token so Takopi can prevent parallel runs
|
||
- Starting the transport loop in `build_and_run`
|
||
|
||
---
|
||
|
||
## CommandBackend
|
||
|
||
```py
|
||
class CommandBackend(Protocol):
|
||
id: str
|
||
description: str
|
||
|
||
async def handle(self, ctx: CommandContext) -> CommandResult | None: ...
|
||
```
|
||
|
||
Command handlers receive a `CommandContext` with:
|
||
|
||
- the raw command text and parsed args
|
||
- the original message + reply metadata
|
||
- `config_path` for the active `takopi.toml` (when known)
|
||
- `plugin_config` from `[plugins.<id>]` (dict, defaults to `{}`)
|
||
- `runtime` (engine/project resolution)
|
||
- `executor` (send messages or run engines)
|
||
|
||
Use `ctx.executor.run_one(...)` or `ctx.executor.run_many(...)` to reuse Takopi's
|
||
engine pipeline. Use `mode="capture"` to collect results and build a custom reply.
|
||
|
||
`ctx.message` and `ctx.reply_to` are `MessageRef` objects with:
|
||
|
||
- `channel_id` (`int | str`, chat/channel id)
|
||
- `message_id` (`int | str`, message id)
|
||
- `thread_id` (`int | str | None`; set when the transport supports threads, like Telegram topics)
|
||
- `raw` (transport-specific payload, may be `None`)
|
||
|
||
Example: key per-thread state by `(ctx.message.channel_id, ctx.message.thread_id)`.
|
||
|
||
---
|
||
|
||
## TransportRuntime helpers
|
||
|
||
`TransportRuntime` keeps transports away from internal router/project types. Key helpers:
|
||
|
||
- `resolve_message(text, reply_text)` → `ResolvedMessage` (prompt, resume token, context)
|
||
- `resolve_engine(engine_override, context)` → `EngineId`
|
||
- `resolve_runner(resume_token, engine_override)` → `ResolvedRunner` (runner + availability info)
|
||
- `resolve_run_cwd(context)` → `Path | None` (raises `ConfigError` for project/worktree issues)
|
||
- `format_context_line(context)` → `str | None`
|
||
- `available_engine_ids()` / `missing_engine_ids()` / `engine_ids` / `default_engine`
|
||
- `project_aliases()`
|
||
- `config_path` (active config path when available)
|
||
- `plugin_config(plugin_id)` → `dict` from `[plugins.<id>]`
|
||
|
||
---
|
||
|
||
## Bridge usage (transport plugins)
|
||
|
||
Most transports can delegate message handling to `handle_message()`. Use
|
||
`TransportRuntime` to resolve messages and select a runner:
|
||
|
||
```py
|
||
from takopi.api import (
|
||
ExecBridgeConfig,
|
||
IncomingMessage,
|
||
RunningTask,
|
||
RunningTasks,
|
||
TransportRuntime,
|
||
handle_message,
|
||
)
|
||
|
||
async def on_message(...):
|
||
resolved = runtime.resolve_message(text=text, reply_text=reply_text)
|
||
entry = runtime.resolve_runner(
|
||
resume_token=resolved.resume_token,
|
||
engine_override=resolved.engine_override,
|
||
)
|
||
context_line = runtime.format_context_line(resolved.context)
|
||
incoming = IncomingMessage(
|
||
channel_id=...,
|
||
message_id=...,
|
||
text=...,
|
||
reply_to=...,
|
||
thread_id=...,
|
||
)
|
||
await handle_message(
|
||
exec_cfg,
|
||
runner=entry.runner,
|
||
incoming=incoming,
|
||
resume_token=resolved.resume_token,
|
||
context=resolved.context,
|
||
context_line=context_line,
|
||
strip_resume_line=runtime.is_resume_line,
|
||
running_tasks=running_tasks,
|
||
on_thread_known=on_thread_known,
|
||
)
|
||
```
|
||
|
||
`handle_message()` implements:
|
||
|
||
- Progress updates and throttling
|
||
- Resume handling
|
||
- Cancellation propagation
|
||
- Final rendering
|
||
|
||
This keeps transport backends thin and consistent with core behavior.
|