This repository has been archived on 2026-05-15. You can view files and clone it, but cannot push or open issues or pull requests.
pi-fanout/README.md

130 lines
4.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pi-fanout
Non-blocking async agent fanout for [pi](https://pi.earendil.dev).
## Problem
The built-in `subagent` tool is powerful, but its `execute()` blocks until **all**
dispatched agents finish. While parallel tasks run concurrently internally, the main
pi session is frozen waiting for the final result.
## Solution
`pi-fanout` turns subagent dispatch into a **true async job queue**:
- `fanout_dispatch` — returns immediately with job IDs; agents run as detached processes
- `fanout_status` — poll running/done/failed counts at any time
- `fanout_collect` — retrieve final output from completed jobs
- `fanout_abort` — kill running jobs on demand
The main pi session stays unblocked. You can do other work, dispatch more jobs,
and collect results whenever theyre ready. When a background job finishes, the
extension sends a `followUp` message into the session so you know its time to
collect.
## Architecture
```
pi main process
├─ pi-fanout extension
│ ├─ JobManager (in-memory + disk state in ~/.pi/fanout/jobs/)
│ └─ Poller (every 2s: check PIDs, notify on completion)
├─ detached pi child (Agent A) ── writes output to job dir
├─ detached pi child (Agent B) ── writes output to job dir
└─ detached pi child (Agent C) ── writes output to job dir
```
Jobs survive pi restarts because:
1. Child processes are **detached** from the parent
2. State is persisted as `meta.json` + `output.jsonl` per job
3. On startup the extension rehydrates old jobs and checks if their PIDs are still alive
## Install
```bash
pi use git:git.vpsj.de/jay/pi-fanout
```
Or clone into your extensions directory and add it to `~/.pi/extensions.json`.
## Usage
```
> fanout_dispatch tasks=[{agent:"worker", task:"Refactor auth.ts"}, {agent:"reviewer", task:"Review auth.ts"}]
Dispatched 2 job(s). IDs:
ltv123-abc
ltv124-def
> ... do other work in the main session ...
> fanout_status
Jobs: 2 total — 0 running, 0 queued, 2 done, 0 failed/aborted
> fanout_collect jobIds=["ltv123-abc","ltv124-def"]
[ltv123-abc] done (exit 0) [claude-sonnet-4]
Refactored auth.ts to use bearer tokens...
---
[ltv124-def] done (exit 0) [claude-sonnet-4]
The refactored auth.ts looks solid. One suggestion: ...
```
## Tools
### `fanout_dispatch`
Parameters:
- `tasks`: array of `{ agent, task, cwd?, model?, tools? }`
- `agentScope`: `"user" | "project" | "both"` (default `"user"`)
Returns: `{ dispatched: string[] }` — job IDs.
### `fanout_status`
Parameters:
- `jobIds?`: filter to specific IDs (omit for all)
- `includeDone?`: include finished jobs in listing (default `true`)
Returns per-job: `id`, `status`, `agent`, `task`, `exitCode`, `pid`, `turns`, `cost`.
### `fanout_collect`
Parameters:
- `jobIds`: array of IDs to collect
Returns per-job: `id`, `status`, `output` (final assistant text), `exitCode`, `usage`, `modelUsed`, `errorMessage`.
### `fanout_abort`
Parameters:
- `jobIds`: array of IDs to kill
Sends `SIGTERM`, then `SIGKILL` after 5s if still running.
## How it works with the agent loop
1. The LLM calls `fanout_dispatch`.
2. The tool returns **in <50ms** with job IDs.
3. The LLM is free to continue the conversation, run other tools, or ask the user.
4. Every 2 seconds the extension polls job PIDs.
5. When a job transitions to `done`/`failed`, the extension calls:
```ts
pi.sendUserMessage(`Fanout job X completed ...`, { deliverAs: "followUp" })
```
This injects a user-style message that triggers a follow-up turn when the agent is idle.
6. The LLM sees the notification and calls `fanout_collect` to retrieve outputs.
7. The LLM acts on the collected results (e.g. synthesize a final answer, dispatch follow-up jobs).
## Limitations / Roadmap
- **No true server push into a running turn**: If pi is mid-stream when a job finishes, the notification is queued as `followUp` and processed when the current turn ends.
- **No job result streaming** into the main session yet. Jobs are collected atomically after completion.
- **No automatic `fanout_collect`**: The LLM must explicitly call it. Future versions could auto-inject a tool-call hint.
- Jobs older than 24h are auto-cleaned on startup.
## License
MIT