feat: add pi runner (#24)
This commit is contained in:
+14
-4
@@ -49,7 +49,7 @@ The orchestrator module containing:
|
||||
- `/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>` ``)
|
||||
- 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)
|
||||
|
||||
@@ -92,6 +92,13 @@ The orchestrator module containing:
|
||||
- Stderr is drained into a bounded tail (debug logging only)
|
||||
- Translation errors abort the run; keep event normalization defensive
|
||||
|
||||
### `runners/pi.py` - Pi runner
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `PiRunner` | Spawns `pi --print --mode json`, streams JSONL, emits takopi events |
|
||||
| `translate_pi_event()` | Normalizes Pi JSONL into the takopi event schema |
|
||||
|
||||
### `model.py` / `runner.py` - Core domain types
|
||||
|
||||
| File | Purpose |
|
||||
@@ -113,6 +120,8 @@ Auto-discovers runner modules in `takopi.runners` that export `BACKEND`.
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `codex.py` | Codex runner (JSONL → takopi events) + per-resume locks |
|
||||
| `claude.py` | Claude runner (JSONL → takopi events) + per-resume locks |
|
||||
| `pi.py` | Pi runner (JSONL → takopi events) + per-resume locks |
|
||||
| `mock.py` | Mock runner for tests/demos |
|
||||
|
||||
### `config.py` - Configuration loading
|
||||
@@ -166,7 +175,7 @@ handle_message() spawned as task with selected runner
|
||||
Send initial progress message (silent)
|
||||
↓
|
||||
runner.run(prompt, resume_token)
|
||||
├── Spawns engine subprocess (e.g., codex exec --json)
|
||||
├── Spawns engine subprocess (e.g., codex exec --json, pi --print --mode json)
|
||||
├── Streams JSONL from stdout
|
||||
├── Normalizes JSONL -> takopi events
|
||||
├── Yields Takopi events (async iterator)
|
||||
@@ -184,8 +193,8 @@ Send/edit final message
|
||||
### Resume Flow
|
||||
|
||||
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> -`)
|
||||
- Router returns first matching token (e.g. `` `claude --resume <id>` `` routes to Claude, `` `pi --session <path>` `` routes to Pi)
|
||||
- Selected runner spawns with resume (e.g. `codex exec --json resume <token> -`, `pi --print --mode json --session <path> <prompt>`)
|
||||
- Per-token lock serializes concurrent resumes on the same thread
|
||||
|
||||
## Error Handling
|
||||
@@ -193,6 +202,7 @@ Same as above; auto-router polls all runners to extract resume tokens:
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| `codex exec` fails (rc != 0) | Emits a warning `action` plus `completed(ok=false, error=...)` |
|
||||
| `pi` fails (rc != 0) | Emits a warning `action` plus `completed(ok=false, error=...)` |
|
||||
| Telegram API error | Logged, edit skipped (progress continues) |
|
||||
| Cancellation | Cancel scope terminates the process group (POSIX) and renders `cancelled` |
|
||||
| Errors in handler | Final render uses `status=error` and preserves resume tokens when known |
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
Below is a concrete implementation spec for adding **Pi (pi-coding-agent CLI)** as a first-class engine in Takopi (v0.4.0).
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
### Goal
|
||||
|
||||
Add a new engine backend **`pi`** so Takopi can:
|
||||
|
||||
* Run Pi non-interactively via the **pi CLI** (`pi --print`).
|
||||
* Stream progress by parsing **`--mode json`** (newline-delimited JSON). Each line is a JSON object.
|
||||
* Support resumable sessions via **`--session <path>`** (Takopi emits a canonical resume line the user can reply with).
|
||||
|
||||
### Non-goals (v1)
|
||||
|
||||
* Interactive TUI flows (session picker, prompts, etc.)
|
||||
* RPC mode (requires a long-running process and JSON commands)
|
||||
|
||||
---
|
||||
|
||||
## UX and behavior
|
||||
|
||||
### Engine selection
|
||||
|
||||
* Existing: `takopi codex`
|
||||
* New: `takopi pi`
|
||||
|
||||
### Resume UX (canonical line)
|
||||
|
||||
Takopi appends a **single backticked** resume line at the end of the message, like:
|
||||
|
||||
```text
|
||||
`pi --session /home/user/.pi/agent/sessions/--repo--/2026-01-02T12-34-56-789Z_abcd.jsonl`
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
* `pi --resume/-r` opens an interactive session picker, so Takopi uses `--session <path>` instead.
|
||||
* The resume token is the **session file path** (JSONL), treated as an opaque string.
|
||||
* If the path contains spaces, the runner will quote it.
|
||||
|
||||
### Non-interactive runs
|
||||
|
||||
Use `--print` and `--mode json` for headless JSONL output.
|
||||
|
||||
Pi does not accept `-- <prompt>` to protect prompts starting with `-`. Takopi prefixes a leading space if the prompt begins with `-` so it is not parsed as a flag.
|
||||
|
||||
---
|
||||
|
||||
## Config additions
|
||||
|
||||
Takopi config lives at either:
|
||||
|
||||
* `.takopi/takopi.toml` (project-local), or
|
||||
* `~/.takopi/takopi.toml` (home).
|
||||
|
||||
Add a new optional `[pi]` section.
|
||||
|
||||
Recommended v1 schema:
|
||||
|
||||
```toml
|
||||
# .takopi/takopi.toml
|
||||
|
||||
default_engine = "pi"
|
||||
|
||||
[pi]
|
||||
cmd = "pi" # optional; defaults to "pi"
|
||||
extra_args = [] # optional list of strings, appended verbatim
|
||||
model = "..." # optional; passed as --model
|
||||
provider = "..." # optional; passed as --provider
|
||||
session_dir = "..." # optional; directory for session files
|
||||
session_title = "pi" # optional; defaults to model or "pi"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
* `extra_args` lets you pass new Pi flags without changing Takopi.
|
||||
* If `session_dir` is omitted, Takopi uses Pi's default session dir:
|
||||
`~/.pi/agent/sessions/--<cwd>--` (with path separators replaced by `-`).
|
||||
|
||||
---
|
||||
|
||||
## Code changes (by file)
|
||||
|
||||
### 1) New file: `src/takopi/runners/pi.py`
|
||||
|
||||
Expose a module-level `BACKEND = EngineBackend(...)`.
|
||||
|
||||
#### Runner invocation
|
||||
|
||||
The runner should launch Pi in headless JSON mode:
|
||||
|
||||
```text
|
||||
pi --print --mode json --session <session.jsonl> <prompt>
|
||||
```
|
||||
|
||||
When resuming, `<session.jsonl>` is the resume token extracted from the chat.
|
||||
|
||||
#### Event translation
|
||||
|
||||
Pi JSONL output is `AgentSessionEvent` (from `@mariozechner/pi-agent-core`).
|
||||
The runner should translate:
|
||||
|
||||
* `tool_execution_start` -> `action` (phase: started)
|
||||
* `tool_execution_end` -> `action` (phase: completed)
|
||||
* `agent_end` -> `completed`
|
||||
|
||||
For the final answer, use the most recent assistant message text (from
|
||||
`message_end` events). For errors, if the assistant stopReason is `error` or
|
||||
`aborted`, emit `completed(ok=false, error=...)`.
|
||||
|
||||
---
|
||||
|
||||
## Installation and auth
|
||||
|
||||
Install the CLI globally:
|
||||
|
||||
```text
|
||||
npm install -g @mariozechner/pi-coding-agent
|
||||
```
|
||||
|
||||
Auth is stored under `~/.pi/agent/auth.json`. Run `pi` once interactively to
|
||||
set up credentials before using Takopi.
|
||||
|
||||
---
|
||||
|
||||
## Known pitfalls
|
||||
|
||||
* `--resume` is interactive; Takopi uses `--session <path>` instead.
|
||||
* Prompts that start with `-` are interpreted as flags by the CLI. Takopi
|
||||
prefixes a space to make them safe.
|
||||
|
||||
---
|
||||
|
||||
If you want, I can also add a sample `takopi.toml` snippet to the README or
|
||||
include a small quickstart section for Pi in the onboarding panel.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Pi `--mode json` event cheatsheet
|
||||
|
||||
`pi --print --mode json` writes **one JSON object per line** (JSONL) with a
|
||||
required `type` field. These are `AgentSessionEvent` objects from
|
||||
`@mariozechner/pi-agent-core`.
|
||||
|
||||
## Top-level event lines
|
||||
|
||||
### `agent_start`
|
||||
|
||||
```json
|
||||
{"type":"agent_start"}
|
||||
```
|
||||
|
||||
### `agent_end`
|
||||
|
||||
```json
|
||||
{"type":"agent_end","messages":[{"role":"assistant","content":[{"type":"text","text":"Done."}],"stopReason":"stop","timestamp":123}]}
|
||||
```
|
||||
|
||||
### `turn_start` / `turn_end`
|
||||
|
||||
```json
|
||||
{"type":"turn_start"}
|
||||
```
|
||||
|
||||
```json
|
||||
{"type":"turn_end","message":{...},"toolResults":[...]}
|
||||
```
|
||||
|
||||
### `message_start` / `message_update` / `message_end`
|
||||
|
||||
```json
|
||||
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":"Working..."}]}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","delta":"...","contentIndex":0}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Done."}],"stopReason":"stop"}}
|
||||
```
|
||||
|
||||
### `tool_execution_start`
|
||||
|
||||
```json
|
||||
{"type":"tool_execution_start","toolCallId":"tool_1","toolName":"bash","args":{"command":"ls"}}
|
||||
```
|
||||
|
||||
### `tool_execution_update`
|
||||
|
||||
```json
|
||||
{"type":"tool_execution_update","toolCallId":"tool_1","toolName":"bash","args":{"command":"ls"},"partialResult":{"content":[{"type":"text","text":"..."}]}}
|
||||
```
|
||||
|
||||
### `tool_execution_end`
|
||||
|
||||
```json
|
||||
{"type":"tool_execution_end","toolCallId":"tool_1","toolName":"bash","result":{"content":[{"type":"text","text":"ok"}],"details":{}},"isError":false}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
* `message_end` with `role = "assistant"` contains the final assistant text.
|
||||
* `assistantMessageEvent` in `message_update` provides streaming deltas.
|
||||
* `tool_execution_*` events map cleanly to Takopi `action` events.
|
||||
@@ -0,0 +1,154 @@
|
||||
# Pi -> Takopi event mapping (spec)
|
||||
|
||||
This document specifies how to add a Pi runner to Takopi by translating
|
||||
Pi CLI `--mode json` JSONL events into Takopi events. The Pi JSONL stream is
|
||||
`AgentSessionEvent` from `@mariozechner/pi-agent-core`.
|
||||
|
||||
The goal is to make Pi feel identical to the Codex/Claude runners from the
|
||||
bridge/renderer point of view while preserving Takopi invariants (stable action
|
||||
ids, per-session serialization, single completed event).
|
||||
|
||||
---
|
||||
|
||||
## 1. Input stream contract (Pi CLI)
|
||||
|
||||
Pi CLI emits **one JSON object per line** (JSONL) when invoked with:
|
||||
|
||||
```
|
||||
pi --print --mode json <prompt>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `--print` is required for non-interactive runs.
|
||||
- `--mode json` outputs all agent events (no TUI banners).
|
||||
- Pi does not support `-- <prompt>`; prompts starting with `-` must be
|
||||
prefixed (Takopi does this automatically).
|
||||
|
||||
---
|
||||
|
||||
## 2. Resume tokens and resume lines
|
||||
|
||||
- Engine id: `pi`
|
||||
- Canonical resume line (embedded in chat):
|
||||
|
||||
```
|
||||
`pi --session <path>`
|
||||
```
|
||||
|
||||
The token is the **session JSONL file path**.
|
||||
|
||||
Why not `--resume`?
|
||||
- `--resume/-r` opens an interactive session picker; it does not accept a
|
||||
session token. Takopi must use `--session <path>` instead.
|
||||
|
||||
---
|
||||
|
||||
## 3. Session lifecycle + serialization
|
||||
|
||||
Takopi requires **serialization per session token**:
|
||||
|
||||
- For new runs (`resume=None`), do **not** acquire a lock until a `started`
|
||||
event is emitted (Takopi emits this as soon as the first JSON event arrives).
|
||||
- Once the session is known, acquire a lock for `pi:<session_path>` and hold it
|
||||
until the run completes.
|
||||
- For resumed runs, acquire the lock immediately on entry.
|
||||
|
||||
---
|
||||
|
||||
## 4. Event translation (Pi JSONL -> Takopi)
|
||||
|
||||
Pi emits `AgentSessionEvent` objects. Only a subset is required for Takopi.
|
||||
|
||||
### 4.1 `tool_execution_start`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"tool_execution_start","toolCallId":"tool_1","toolName":"bash","args":{"command":"ls"}}
|
||||
```
|
||||
|
||||
Mapping:
|
||||
- Emit `action` with `phase="started"`.
|
||||
- `action.id = toolCallId`.
|
||||
- `action.kind` from tool name (see section 5).
|
||||
- `action.title` derived from tool + args.
|
||||
|
||||
### 4.2 `tool_execution_end`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"tool_execution_end","toolCallId":"tool_1","toolName":"bash","result":{...},"isError":false}
|
||||
```
|
||||
|
||||
Mapping:
|
||||
- Emit `action` with `phase="completed"`.
|
||||
- `ok = !isError`.
|
||||
- Carry `result` and `isError` in `detail` for debugging.
|
||||
|
||||
### 4.3 `message_end` (assistant)
|
||||
|
||||
Pi emits message lifecycle events. For `message_end` where `message.role == "assistant"`:
|
||||
|
||||
- Store the latest assistant text as the **final answer fallback**.
|
||||
- If `stopReason` is `error` or `aborted`, store `errorMessage`.
|
||||
- Capture `usage` for `completed.usage`.
|
||||
|
||||
### 4.4 `agent_end`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"agent_end","messages":[...]}
|
||||
```
|
||||
|
||||
Mapping:
|
||||
- Emit a single `completed` event:
|
||||
- `ok = true` unless the last assistant message has `stopReason` `error` or `aborted`.
|
||||
- `answer = last assistant text` (from `message_end` or `agent_end.messages`).
|
||||
- `error = errorMessage` if present.
|
||||
- `resume = ResumeToken(engine="pi", value=session_path)`.
|
||||
- `usage = last assistant usage`.
|
||||
|
||||
### 4.5 Other events
|
||||
|
||||
Ignore unknown events. If a JSONL line is malformed, emit a warning action and
|
||||
continue (default `JsonlSubprocessRunner` behavior).
|
||||
|
||||
---
|
||||
|
||||
## 5. Tool name -> ActionKind mapping heuristics
|
||||
|
||||
Pi tool names are lower-case by default. Suggested mapping:
|
||||
|
||||
| Tool name | ActionKind | Title logic |
|
||||
| --- | --- | --- |
|
||||
| `bash` | `command` | `args.command` |
|
||||
| `edit`, `write` | `file_change` | `args.path` |
|
||||
| `read` | `tool` | `read: <path>` |
|
||||
| `grep` | `tool` | `grep: <pattern>` |
|
||||
| `find` | `tool` | `find: <pattern>` |
|
||||
| `ls` | `tool` | `ls: <path>` |
|
||||
| (default) | `tool` | tool name |
|
||||
|
||||
For `file_change`, include `detail.changes = [{"path": <path>, "kind": "update"}]`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Usage mapping
|
||||
|
||||
Takopi `completed.usage` should mirror Pi's assistant `usage` object without
|
||||
transformation.
|
||||
|
||||
---
|
||||
|
||||
## 7. Suggested Takopi config keys
|
||||
|
||||
A minimal TOML config for Pi:
|
||||
|
||||
```toml
|
||||
[pi]
|
||||
cmd = "pi"
|
||||
model = "..."
|
||||
provider = "..."
|
||||
extra_args = []
|
||||
```
|
||||
|
||||
Use `extra_args` for any newer Pi CLI flags not explicitly mapped.
|
||||
@@ -23,7 +23,7 @@ Out of scope for v0.4.0:
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
- **EngineId**: string identifier of an engine (e.g., `"codex"`).
|
||||
- **EngineId**: string identifier of an engine (e.g., `"codex"`, `"claude"`, `"pi"`).
|
||||
- **Runner**: Takopi adapter that executes an engine process and yields **Takopi events**.
|
||||
- **Thread**: a single engine-side conversation, identified in Takopi by a **ResumeToken**.
|
||||
- **ResumeToken**: Takopi-owned thread identifier `{ engine: EngineId, value: str }`.
|
||||
@@ -41,6 +41,7 @@ The canonical ResumeLine embedded in chat MUST be the engine’s CLI resume comm
|
||||
|
||||
- `codex resume <id>`
|
||||
- `claude --resume <id>`
|
||||
- `pi --session <path>`
|
||||
|
||||
Takopi MUST treat the runner as authoritative for:
|
||||
|
||||
@@ -347,7 +348,7 @@ Decision (v0.4.0):
|
||||
* 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.
|
||||
* Users MAY prefix the first non-empty line with `/{engine}` (e.g. `/claude`, `/codex`, or `/pi`) 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user