pipeline-moe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/bin/pipeline-moe.mjs +40 -0
- package/package.json +68 -0
- package/presets/2106BUILD.json +163 -0
- package/presets/CHEAPBUILD.json +97 -0
- package/presets/FREEROOM.json +96 -0
- package/presets/Versa.json +145 -0
- package/presets/cloud-main.json +76 -0
- package/presets/cloud-sprint.json +70 -0
- package/presets/local-default.json +152 -0
- package/presets/main.json +147 -0
- package/presets/mainmix.json +145 -0
- package/src/circuit-breaker.ts +141 -0
- package/src/config.ts +46 -0
- package/src/custom-tools/arxiv-search.ts +186 -0
- package/src/custom-tools/check-room.ts +45 -0
- package/src/custom-tools/destroy-room.ts +45 -0
- package/src/custom-tools/index.ts +75 -0
- package/src/custom-tools/spawn-room.ts +99 -0
- package/src/custom-tools/stop-room.ts +50 -0
- package/src/custom-tools/web-read.ts +104 -0
- package/src/custom-tools/web-search.ts +144 -0
- package/src/custom-tools/youcom-search.ts +230 -0
- package/src/custom-tools/youtube-transcript.ts +124 -0
- package/src/local-model-lock.ts +52 -0
- package/src/model.ts +131 -0
- package/src/orchestrator.ts +59 -0
- package/src/participant.ts +423 -0
- package/src/path-guard.ts +13 -0
- package/src/personas.ts +480 -0
- package/src/receipts.ts +120 -0
- package/src/registry.ts +317 -0
- package/src/room-manager.ts +505 -0
- package/src/room.ts +1657 -0
- package/src/sandbox-tools.ts +141 -0
- package/src/server.ts +1846 -0
- package/src/sse.ts +86 -0
- package/src/sshfs.ts +139 -0
- package/src/store.ts +79 -0
- package/src/types.ts +131 -0
- package/src/validation.ts +36 -0
- package/tsconfig.json +15 -0
- package/web/dist/assets/index-CmMGhMKG.css +1 -0
- package/web/dist/assets/index-LAGqbZII.js +41 -0
- package/web/dist/index.html +13 -0
- package/web/tsconfig.json +21 -0
package/.env.example
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Workspace the agents read/edit (shared filesystem). Absolute path
|
|
2
|
+
# recommended. Defaults to the directory the server is started from.
|
|
3
|
+
# WORKSPACE_DIR=/path/to/your/project
|
|
4
|
+
|
|
5
|
+
# HTTP port for the Express API.
|
|
6
|
+
PORT=5300
|
|
7
|
+
|
|
8
|
+
# Model in "provider/id" form. When unset, the backend prefers the `local`
|
|
9
|
+
# provider from ~/.pi/agent/models.json (llama-server :5000). Set this
|
|
10
|
+
# explicitly to be safe and avoid ever falling back to a cloud model.
|
|
11
|
+
# PIPELINE_MODEL=local/your-model.gguf
|
|
12
|
+
|
|
13
|
+
# Thinking level for sub-agents: off | minimal | low | medium | high | xhigh
|
|
14
|
+
PIPELINE_THINKING=medium
|
|
15
|
+
|
|
16
|
+
# SearXNG instance for the web_search tool (self-hosted or public; must have
|
|
17
|
+
# the JSON output format enabled). The tool reports an error when unset.
|
|
18
|
+
# SEARXNG_URL=https://searx.example.org
|
|
19
|
+
|
|
20
|
+
# Allow cloud providers (Anthropic, DeepSeek…). Off by default: this stack is
|
|
21
|
+
# local-first, and cloud models are hidden/rejected unless explicitly enabled.
|
|
22
|
+
# PIPELINE_ALLOW_CLOUD=1
|
|
23
|
+
|
|
24
|
+
# CORS origins (comma-separated). Defaults to localhost:5310,localhost:5300.
|
|
25
|
+
# Set this if you need to allow other origins (e.g., a remote frontend).
|
|
26
|
+
# PIPELINE_CORS_ORIGINS=http://localhost:5310,http://localhost:5300
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DAXZEIT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Pipeline-MoE
|
|
2
|
+
|
|
3
|
+
A **multi-agent chat room** you can run entirely on local models. Orchestrates
|
|
4
|
+
**N stateful `pi` agent sessions** (one per persona, same model, different
|
|
5
|
+
system prompts + tool sets) over a **shared workspace**, routes `@mentions`
|
|
6
|
+
through a **serial queue**, and streams everything to its clients over **SSE**.
|
|
7
|
+
|
|
8
|
+
Two clients ship with it, built on a shared framework-agnostic core
|
|
9
|
+
(`@pipeline-moe/client-core`):
|
|
10
|
+
|
|
11
|
+
- **TUI** (`packages/tui`) — the flagship terminal client: multi-room, slash
|
|
12
|
+
commands, live markdown-rendered streaming, line-accurate scrollback, lineup
|
|
13
|
+
management, provider OAuth.
|
|
14
|
+
- **Web UI** (`web/`) — the same rooms over React + Vite.
|
|
15
|
+
|
|
16
|
+
Local-first by policy: cloud providers are hidden and rejected unless you
|
|
17
|
+
explicitly opt in with `PIPELINE_ALLOW_CLOUD=1`.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
TUI (Ink) Web UI (React + Vite)
|
|
21
|
+
╰──────── @pipeline-moe/client-core ────────╯
|
|
22
|
+
│ REST + SSE
|
|
23
|
+
▼
|
|
24
|
+
Express backend ──► Registry of pi AgentSession instances
|
|
25
|
+
serial queue scout / builder / auditor / scribe / tester …
|
|
26
|
+
routing @mentions │ each = createAgentSession(persona, tools)
|
|
27
|
+
workspace diff ▼
|
|
28
|
+
llama-server :5000 (any local GGUF, --parallel 1)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Each participant is a real `pi` `AgentSession`: it keeps its **own conversation
|
|
32
|
+
memory** (stateful) and gets **real tools** (read/bash/edit/write, gated per
|
|
33
|
+
persona). The shared room transcript is threaded into each agent's prompt so they
|
|
34
|
+
see each other's messages; they share the filesystem so edits are visible across
|
|
35
|
+
agents. After every agent turn the workspace is diffed to produce a **work
|
|
36
|
+
receipt**.
|
|
37
|
+
|
|
38
|
+
## Run
|
|
39
|
+
|
|
40
|
+
**From npm** (server + bundled web UI, no clone needed):
|
|
41
|
+
```bash
|
|
42
|
+
npx pipeline-moe serve # API + web UI on :5300, workspace = current dir
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**From source:**
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/DAXZEIT/pipeline-moe && cd pipeline-moe
|
|
48
|
+
npm install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Server:**
|
|
52
|
+
```bash
|
|
53
|
+
npm start # backend on :5300 (llama-server assumed running on :5000)
|
|
54
|
+
npm run dev # backend + web UI dev server (:5310)
|
|
55
|
+
bash start.sh # full launch: llama-server → backend → web UI, health-gated
|
|
56
|
+
# (set LLAMA_SCRIPT to your llama-server launch script)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Terminal client:**
|
|
60
|
+
```bash
|
|
61
|
+
npm -C packages/tui start # connect to localhost:5300
|
|
62
|
+
npm -C packages/tui start -- --server http://host:5300 --room default
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Defaults: port `5300`, workspace = current directory, model = pi's default
|
|
66
|
+
(your local provider in `~/.pi/agent/models.json`). A `.env` in the working
|
|
67
|
+
directory is loaded automatically — copy `.env.example` and adjust.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
| Method | Path | Body | Purpose |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| GET | `/api/health` | — | liveness |
|
|
74
|
+
| GET | `/api/events` | — | **SSE** stream (roster, message, token, status, receipt, notice, turn, etc.) |
|
|
75
|
+
| GET | `/api/participants` | — | roster snapshot |
|
|
76
|
+
| POST | `/api/participants` | `{name, systemPrompt, tools?, color?, icon?, id?}` | **create** a participant |
|
|
77
|
+
| PATCH | `/api/participants/:id` | `{active: boolean}` or `{parallel: boolean}` | **activate/deactivate** or **toggle parallel** |
|
|
78
|
+
| DELETE | `/api/participants/:id` | — | **kick** |
|
|
79
|
+
| POST | `/api/participants/:id/compact` | — | **compact** agent session (free context tokens) |
|
|
80
|
+
| POST | `/api/participants/reorder` | `{order: [id, …]}` | **reorder** roster |
|
|
81
|
+
| GET | `/api/transcript` | — | full transcript |
|
|
82
|
+
| GET | `/api/conversations` | — | list saved conversations |
|
|
83
|
+
| POST | `/api/conversations` | `{name?}` | create a new conversation |
|
|
84
|
+
| POST | `/api/conversations/:id/load` | — | load a saved conversation |
|
|
85
|
+
| PATCH | `/api/conversations/:id` | `{name: string}` | rename a conversation |
|
|
86
|
+
| DELETE | `/api/conversations/:id` | — | delete a conversation |
|
|
87
|
+
| POST | `/api/messages` | `{text, images?: string[]}` | post to the room (returns 202; results stream over SSE) |
|
|
88
|
+
| POST | `/api/messages/steer` | `{text, target}` | steer a running agent mid-turn (409 if not running) |
|
|
89
|
+
| GET | `/api/participants/:id/export` | — | download session as HTML (attachment) |
|
|
90
|
+
| GET | `/api/participants/:id/export-jsonl` | — | download session as JSONL (attachment) |
|
|
91
|
+
| POST | `/api/abort` | — | abort the currently running agent |
|
|
92
|
+
| GET | `/api/media/:filename` | — | serve a saved image |
|
|
93
|
+
| GET | `/api/workspace` | — | list workspace files |
|
|
94
|
+
| GET | `/api/settings` | — | room settings |
|
|
95
|
+
| PATCH | `/api/settings` | `{chaining?: boolean, defaultAgent?: string}` | update room settings |
|
|
96
|
+
|
|
97
|
+
## SSE Events
|
|
98
|
+
|
|
99
|
+
| Event | Payload | Description |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `roster` | `RosterItem[]` | full roster snapshot (on connect and on any change) |
|
|
102
|
+
| `message` | `{index, author, authorName, text, ts, question?, images?}` | a completed transcript line |
|
|
103
|
+
| `token` | `{id, delta}` | streaming text delta from an agent |
|
|
104
|
+
| `activity` | `ActivityEvent` | tool-call start/end (live process visibility) |
|
|
105
|
+
| `reasoning` | `{id, delta}` | streaming thinking delta (ephemeral) |
|
|
106
|
+
| `status` | `{id, status, contextUsage?, sessionStats?, retry?}` | participant status (`idle`, `active`, `thinking`, `working`, `compacting`, `retrying`). After each turn, `contextUsage` and `sessionStats` included when available. During retries, `retry` metadata is included.
|
|
107
|
+
| `receipt` | `{participantId, created[], modified[], deleted[]}` | work receipt (filesystem diff) |
|
|
108
|
+
| `notice` | `{msg, level}` | informational/error notice |
|
|
109
|
+
| `turn` | `{phase, targets?, askerId?, question?}` | routing turn lifecycle |
|
|
110
|
+
| `workspace` | `FileEntry[]` | live workspace file listing |
|
|
111
|
+
| `settings` | `{chaining, defaultAgent}` | room settings change |
|
|
112
|
+
| `transcript` | `TranscriptEntry[]` | full transcript replacement (on conversation switch) |
|
|
113
|
+
| `conversations` | `{conversations, currentId}` | saved-conversation list + current id |
|
|
114
|
+
|
|
115
|
+
**Turn phases:** `start`, `end`, `chain`, `parallel`, `pause`, `resume`
|
|
116
|
+
|
|
117
|
+
## Features
|
|
118
|
+
|
|
119
|
+
### `@mention` Routing
|
|
120
|
+
|
|
121
|
+
`@all` or no mention → every active participant. `@scout @auditor` → those agents,
|
|
122
|
+
in order. An agent can hand work to another by writing `@<id>` explicitly in its
|
|
123
|
+
reply — only the `@` prefix triggers a handoff. Agents can refer to each other by
|
|
124
|
+
name (e.g. "the builder") in discussion without triggering routing.
|
|
125
|
+
|
|
126
|
+
**Last-paragraph parsing:** Only the last paragraph of an agent's reply is scanned
|
|
127
|
+
for `@mentions`. Mid-text references like "as @builder mentioned" don't trigger
|
|
128
|
+
chains — only the final paragraph is treated as a routing signal. This prevents
|
|
129
|
+
accidental chaining when an agent references another agent in its reasoning.
|
|
130
|
+
|
|
131
|
+
**Chain budget:** Each turn has a chain hop budget of 8 (configurable as
|
|
132
|
+
`MAX_CHAIN_HOPS` in `Room`). If the budget is exhausted during chaining, further
|
|
133
|
+
chains stop and a notice is emitted. This prevents infinite loops from agents
|
|
134
|
+
that hallucinate `@mentions` in their replies. The budget resets at the start of
|
|
135
|
+
each turn.
|
|
136
|
+
|
|
137
|
+
### Parallel Agents
|
|
138
|
+
|
|
139
|
+
A participant can be toggled as "parallel" — when routed alongside other agents,
|
|
140
|
+
they run in the same wave instead of sequentially. Toggled via `PATCH /api/participants/:id`
|
|
141
|
+
with `{parallel: true}`.
|
|
142
|
+
|
|
143
|
+
### `ask_user` — Agent-Initiated Questions
|
|
144
|
+
|
|
145
|
+
Any agent can call the `ask_user` tool to pause the pipeline and ask the user a
|
|
146
|
+
clarifying question. The pipeline enters a "paused" state (SSE event:
|
|
147
|
+
`turn: {phase: "pause", askerId, question}`) and holds the remaining queue. The
|
|
148
|
+
user's response is routed back to the asking agent, and the held queue resumes.
|
|
149
|
+
|
|
150
|
+
Nested questions are supported (an agent can ask again during a resumed turn).
|
|
151
|
+
|
|
152
|
+
**Escape hatches:**
|
|
153
|
+
- `/cancel` — cancels the pause and drains the held queue normally
|
|
154
|
+
- `POST /api/abort` — aborts the current agent (also clears a pause)
|
|
155
|
+
|
|
156
|
+
### Per-Agent Compaction
|
|
157
|
+
|
|
158
|
+
**Manual:** `POST /api/participants/:id/compact` or `/compact @agent` slash
|
|
159
|
+
command. Calls `AgentSession.compact()` and returns the token count before
|
|
160
|
+
compaction. The agent's status changes to `compacting` during the operation.
|
|
161
|
+
|
|
162
|
+
**Automatic:** Each agent session is configured with auto-compaction enabled.
|
|
163
|
+
When an agent's context approaches its limit (90K tokens for a 128K window),
|
|
164
|
+
pi automatically compacts the session. The UI receives a `status: "compacting"`
|
|
165
|
+
event during the operation.
|
|
166
|
+
|
|
167
|
+
**Role-aware compaction:** Each persona can define `compactionInstructions` —
|
|
168
|
+
a short directive (max 500 chars) telling the compaction what to preserve vs
|
|
169
|
+
discard. The 7 seed personas come with tailored instructions:
|
|
170
|
+
|
|
171
|
+
| Persona | Instruction |
|
|
172
|
+
|---|---|
|
|
173
|
+
| scout | Preserve file paths, structural observations, anomalies. Discard dead-ends. |
|
|
174
|
+
| builder | Preserve code changes, bugs, architectural decisions. Discard failed attempts. |
|
|
175
|
+
| auditor | Preserve findings, severity assessments, verification status. Discard clean reads. |
|
|
176
|
+
| scribe | Preserve documentation written, memory updates, knowledge distilled. Discard context reads. |
|
|
177
|
+
| planner | Preserve plans, steps, architectural decisions. Discard source reads for verification. |
|
|
178
|
+
| tester | Preserve test results, pass/fail counts, bugs found. Discard superseded runs. |
|
|
179
|
+
| fetcher | Preserve URLs and key findings. Discard failed fetches and retry traces. |
|
|
180
|
+
|
|
181
|
+
Set via the persona editor (textarea below system prompt) or `PATCH /api/participants/:id`
|
|
182
|
+
with `{compactionInstructions: "..."}`. Null or empty string clears the override.
|
|
183
|
+
|
|
184
|
+
**SDK limitation:** Custom instructions only apply to *manual* compaction via
|
|
185
|
+
`session.compact(customInstructions)`. Auto-compaction uses default instructions —
|
|
186
|
+
the SDK's `session_before_compact` event doesn't expose `customInstructions`
|
|
187
|
+
(known pi SDK limitation, requires SDK update to fix).
|
|
188
|
+
|
|
189
|
+
### Per-Agent Thinking Level
|
|
190
|
+
|
|
191
|
+
Each agent can override the global thinking level (set by `PIPELINE_THINKING` env
|
|
192
|
+
var, defaults to `"medium"`). Set via the persona editor or `PATCH /api/participants/:id`
|
|
193
|
+
with `{thinkingLevel: "high"}`.
|
|
194
|
+
|
|
195
|
+
**Available levels:** `off`, `minimal`, `low`, `medium`, `high`, `xhigh`
|
|
196
|
+
|
|
197
|
+
**Fallback:** When a persona has no `thinkingLevel` set, it inherits the global
|
|
198
|
+
config value. Setting `thinkingLevel` to `null` or `""` via PATCH resets the
|
|
199
|
+
per-agent override and reverts to the global default.
|
|
200
|
+
|
|
201
|
+
**Data flow:** `Persona.thinkingLevel ?? config.thinkingLevel` →
|
|
202
|
+
`createAgentSession({ thinkingLevel })` → session uses it for reasoning effort.
|
|
203
|
+
|
|
204
|
+
**Fast path:** If `thinkingLevel` is the *only* field in the PATCH payload, the
|
|
205
|
+
backend calls `session.setThinkingLevel()` in-place — no session recreation, no
|
|
206
|
+
cursor reset, no "room is busy" block. Takes effect on the next turn. Combined
|
|
207
|
+
patches (e.g. thinkingLevel + name) still take the heavy dispose+recreate path.
|
|
208
|
+
|
|
209
|
+
**Available levels filter:** `GET /api/participants/:id` returns
|
|
210
|
+
`availableThinkingLevels` from `session.getAvailableThinkingLevels()`. The
|
|
211
|
+
EditAgent selector only shows levels the model actually supports — falls back to
|
|
212
|
+
all 6 if the session returns nothing.
|
|
213
|
+
|
|
214
|
+
The PATCH endpoint validates values against the allowlist — invalid values return 400.
|
|
215
|
+
|
|
216
|
+
### Context Usage per Agent
|
|
217
|
+
|
|
218
|
+
After each agent turn, the UI shows a progress bar in the Roster with the agent's
|
|
219
|
+
current context token usage (e.g. "42K / 128K"), color-coded by threshold.
|
|
220
|
+
|
|
221
|
+
**Data flow:** `AgentSession.getContextUsage()` → `Participant.getContextUsage()` →
|
|
222
|
+
`Room.runAgent()` broadcasts via SSE `status` event (piggyback, no new event type).
|
|
223
|
+
|
|
224
|
+
**Color thresholds** (inclusive boundaries):
|
|
225
|
+
- Green: < 50%
|
|
226
|
+
- Yellow: 50–75%
|
|
227
|
+
- Orange: 75–90%
|
|
228
|
+
- Red: > 90%
|
|
229
|
+
|
|
230
|
+
**Warning:** When usage exceeds 80%, the bar pulses (CSS animation `ctx-pulse`)
|
|
231
|
+
to alert the operator that compaction may be needed before the loop completes.
|
|
232
|
+
|
|
233
|
+
**Visibility:** Bars appear after the agent's first completed turn (SSE event
|
|
234
|
+
must carry `contextUsage`). Mid-turn status events (e.g. `working`, `compacting`)
|
|
235
|
+
don't include `contextUsage` — the last known value persists through the turn.
|
|
236
|
+
Bars are hidden when `contextUsage` is `undefined` (fresh load before any turn).
|
|
237
|
+
|
|
238
|
+
**Why piggyback on `status` and not a new event?**
|
|
239
|
+
The `idle` status already fires at the end of every turn. Adding a new SSE event
|
|
240
|
+
type would require a new listener in the frontend. Extending the existing `status`
|
|
241
|
+
event's payload is simpler — the frontend already processes status events and
|
|
242
|
+
updates roster items.
|
|
243
|
+
|
|
244
|
+
### Vision — Image Support
|
|
245
|
+
|
|
246
|
+
Users can send images alongside text messages. Images are saved as hashed files
|
|
247
|
+
in the workspace `media/` directory and served via `GET /api/media/:filename`.
|
|
248
|
+
|
|
249
|
+
- **Paste or drag-drop** images in the UI (clipboard and drag-drop handlers on
|
|
250
|
+
the Composer)
|
|
251
|
+
- Images stored as `media/<md5hash>.<ext>` in the transcript
|
|
252
|
+
- Clicking a thumbnail opens full-size
|
|
253
|
+
- JSON body of `POST /api/messages` accepts `{text, images?: string[]}` where
|
|
254
|
+
images are base64 data URIs
|
|
255
|
+
|
|
256
|
+
### Conversations
|
|
257
|
+
|
|
258
|
+
Rooms can save and switch between multiple conversations. The transcript is
|
|
259
|
+
persisted as a session file, and `GET /api/conversations` lists all saved ones.
|
|
260
|
+
|
|
261
|
+
### Workspace Diffing (Work Receipts)
|
|
262
|
+
|
|
263
|
+
After every agent turn, the workspace is diffed to produce a work receipt
|
|
264
|
+
(`created[]`, `modified[]`, `deleted[]`). Receipts are broadcast via SSE and
|
|
265
|
+
displayed in the UI.
|
|
266
|
+
|
|
267
|
+
### Slash Commands
|
|
268
|
+
|
|
269
|
+
| Command | Effect |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `/kick @x` | Remove a participant |
|
|
272
|
+
| `/activate @x` | Activate a deactivated participant |
|
|
273
|
+
| `/deactivate @x` | Deactivate a participant |
|
|
274
|
+
| `/compact @x` | Compact an agent's context |
|
|
275
|
+
| `/cancel` | Cancel a paused question and drain the held queue |
|
|
276
|
+
|
|
277
|
+
### Session Stats per Agent
|
|
278
|
+
|
|
279
|
+
After each agent turn, the UI shows a compact stats line in the Roster with token
|
|
280
|
+
breakdown and cache efficiency (e.g. "42Ki · 1.2Ko · cache 93% · 3 tools"). Full
|
|
281
|
+
numbers in a tooltip.
|
|
282
|
+
|
|
283
|
+
**Data flow:** `AgentSession.getSessionStats()` → `Participant.getSessionStats()` →
|
|
284
|
+
`Room.runAgent()` / `followUpAgent()` broadcasts via SSE `status` event alongside
|
|
285
|
+
`contextUsage`.
|
|
286
|
+
|
|
287
|
+
**Cache percentage** is the most operationally useful number — it shows KV cache
|
|
288
|
+
hit ratio. A low percentage means the agent is repaying prefill on every turn.
|
|
289
|
+
|
|
290
|
+
### Mid-Turn Steering
|
|
291
|
+
|
|
292
|
+
When an agent is running, the operator can send a redirection message via `steer()`
|
|
293
|
+
instead of aborting. The Composer shows "↪ Steer @id" (amber button) alongside
|
|
294
|
+
"■ Stop" when `turnActive` is true.
|
|
295
|
+
|
|
296
|
+
**Data flow:** `POST /api/messages/steer` with `{ text, target }` →
|
|
297
|
+
`Room.steer(targetId, text)` → posts `↳ steered @id: text` to the transcript →
|
|
298
|
+
`Participant.steer(text)` → `session.steer(text)` queues the message.
|
|
299
|
+
|
|
300
|
+
The agent sees the steer between tool calls — it doesn't interrupt current tool
|
|
301
|
+
execution. A "steer sent" flash appears for 2 seconds, then clears.
|
|
302
|
+
|
|
303
|
+
**Error handling:** 409 if the agent is not running (idle), 404 if not found.
|
|
304
|
+
|
|
305
|
+
### Work Receipts (Context Injection)
|
|
306
|
+
|
|
307
|
+
In addition to workspace diff receipts (filesystem changes broadcast via SSE),
|
|
308
|
+
the room injects structured work receipts into downstream agents' context using
|
|
309
|
+
`session.sendCustomMessage()`. After an agent turn produces file changes, the
|
|
310
|
+
next agent in the queue receives a compact `work_receipt` custom message
|
|
311
|
+
(`display: false`) summarizing what changed — e.g. "Builder created: foo.ts,
|
|
312
|
+
bar.ts; modified: baz.ts".
|
|
313
|
+
|
|
314
|
+
This gives downstream agents filesystem awareness without requiring them to
|
|
315
|
+
re-discover changes from the transcript. The receipt is injected in `drainQueue()`
|
|
316
|
+
after the result is posted.
|
|
317
|
+
|
|
318
|
+
**Caveat:** `sendCustomMessage({ display: false })` messages still consume context
|
|
319
|
+
tokens. In a chain of 8 agents, up to 7 receipts can accumulate per turn.
|
|
320
|
+
Keep receipts compact. Receipts are invisible to the operator (not shown in the
|
|
321
|
+
transcript) but occupy space in the agent's context — monitor token growth on
|
|
322
|
+
downstream agents in long chains.
|
|
323
|
+
|
|
324
|
+
### Session Export
|
|
325
|
+
|
|
326
|
+
**HTML:** Export an agent's session as a self-contained HTML file via
|
|
327
|
+
`GET /api/participants/:id/export`. The download button (⬇) in the Roster actions
|
|
328
|
+
row triggers the download.
|
|
329
|
+
|
|
330
|
+
**Data flow:** `session.exportToHtml()` → writes HTML file to disk → server reads
|
|
331
|
+
and returns with `Content-Disposition: attachment`.
|
|
332
|
+
|
|
333
|
+
Filename format: `{id}-{timestamp}.html` (colons and dots sanitized).
|
|
334
|
+
|
|
335
|
+
**JSONL:** Export an agent's session as JSONL (one JSON object per line) via
|
|
336
|
+
`GET /api/participants/:id/export-jsonl`. Useful for post-mortem analysis,
|
|
337
|
+
dataset extraction, and replay. The Roster has a secondary export button for
|
|
338
|
+
JSONL format. Returns the file as `Content-Disposition: attachment` with
|
|
339
|
+
`application/x-ndjson` content type.
|
|
340
|
+
|
|
341
|
+
### Retry Awareness
|
|
342
|
+
|
|
343
|
+
When pi auto-retries after transient errors (e.g., rate limits on remote models),
|
|
344
|
+
the UI shows a `(attempt/maxAttempts — errorMessage)` indicator in amber in the
|
|
345
|
+
Roster. The agent's status changes to `retrying` during the retry delay.
|
|
346
|
+
|
|
347
|
+
**Data flow:** `auto_retry_start` event → `Participant.onEvent()` emits `retrying`
|
|
348
|
+
status with metadata → SSE `status` event → Roster renders retry indicator.
|
|
349
|
+
|
|
350
|
+
### followUp() — Self-Chaining
|
|
351
|
+
|
|
352
|
+
When an agent asks a question via `ask_user` and the user responds, the answer is
|
|
353
|
+
delivered via `followUp()` instead of `prompt()`. This guarantees the answer is the
|
|
354
|
+
next thing the agent processes — no Room routing, no context rebuild from transcript.
|
|
355
|
+
|
|
356
|
+
**Data flow:** User answer → `Room.followUpAgent(asker, { text, images })` →
|
|
357
|
+
`Participant.followUp(text, images)` → `session.followUp(text, images)` → agent
|
|
358
|
+
processes the answer directly from its session memory.
|
|
359
|
+
|
|
360
|
+
**Implementation note:** `runAgent()` and `followUpAgent()` are thin wrappers around
|
|
361
|
+
a shared `executeAgent(target, context, mode)` method — only the call to
|
|
362
|
+
`target.run()` vs `target.followUp()` differs. The common path (snapshot →
|
|
363
|
+
execute → stats → receipt) lives in one place.
|
|
364
|
+
|
|
365
|
+
## Custom Tools (Extension System)
|
|
366
|
+
|
|
367
|
+
The `src/custom-tools/` directory is a drop-a-file registry for agent tools.
|
|
368
|
+
Each tool is a `ToolDefinition` — same type used by Pi's extension system.
|
|
369
|
+
Adding a new tool is: create the `.ts` file, register it in `index.ts`, add the
|
|
370
|
+
name to `VALID_TOOLS` and `ALL_TOOLS`.
|
|
371
|
+
|
|
372
|
+
**How it works:**
|
|
373
|
+
1. `buildCustomTools(allowlist)` — checks the agent's `tools` allowlist against
|
|
374
|
+
registered tools, returns only the tools that agent is permitted to use
|
|
375
|
+
2. Custom tools are merged into `customTools` in the session config alongside
|
|
376
|
+
confined tools (sandbox-tools)
|
|
377
|
+
3. Opt-in via `persona.tools` allowlist — scout gets `web_search` by default,
|
|
378
|
+
other agents need it added manually
|
|
379
|
+
|
|
380
|
+
**First tool: `web_search`** — SearXNG via HTTP GET to
|
|
381
|
+
`$SEARXNG_URL/search?q=...&format=json` (set `SEARXNG_URL` in `.env` to your
|
|
382
|
+
instance; the JSON output format must be enabled). No external dependencies,
|
|
383
|
+
pure Node `fetch()`. Parameters: `query` (required), `limit` (1-20, default 5),
|
|
384
|
+
`categories` (optional). Returns formatted results (title, URL, snippet —
|
|
385
|
+
truncated at 200 chars). 15s timeout on fetch. Graceful error handling (network
|
|
386
|
+
error, HTTP error, abort, empty results).
|
|
387
|
+
|
|
388
|
+
## Tools per Persona
|
|
389
|
+
|
|
390
|
+
pi built-in tool names: `read, bash, edit, write, grep, find, ls`. Gating is a
|
|
391
|
+
plain allowlist passed to `createAgentSession({ tools })` — no permission shim.
|
|
392
|
+
The `ask_user` tool is available to **all** agents. Custom tools are opt-in via
|
|
393
|
+
the `tools` allowlist — `web_search` is available but only scout gets it by
|
|
394
|
+
default.
|
|
395
|
+
|
|
396
|
+
| Persona | Tools |
|
|
397
|
+
|---|---|
|
|
398
|
+
| scout | read, grep, find, ls, web_search, ask_user |
|
|
399
|
+
| builder | read, write, edit, bash, grep, find, ls, ask_user |
|
|
400
|
+
| auditor | read, grep, find, ls, ask_user |
|
|
401
|
+
| scribe | read, write, edit, grep, find, ls, ask_user |
|
|
402
|
+
| planner | read, grep, find, ls, ask_user |
|
|
403
|
+
| tester | read, bash, grep, find, ls, ask_user |
|
|
404
|
+
| fetcher | read, bash, write, grep, find, ls, web_read, ask_user |
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pipeline-moe CLI — `pipeline-moe serve` starts the server (API + web UI when
|
|
3
|
+
// a build is bundled). The stack runs TypeScript directly through tsx, so no
|
|
4
|
+
// build step is needed at install time.
|
|
5
|
+
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
import { dirname, resolve } from "node:path"
|
|
8
|
+
import { readFileSync } from "node:fs"
|
|
9
|
+
|
|
10
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
11
|
+
const cmd = process.argv[2] ?? "serve"
|
|
12
|
+
|
|
13
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8"))
|
|
15
|
+
console.log(pkg.version)
|
|
16
|
+
process.exit(0)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
20
|
+
console.log(`pipeline-moe — local-first multi-agent chat room
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
pipeline-moe serve Start the server (default; port 5300, PORT to change)
|
|
24
|
+
pipeline-moe --version Print version
|
|
25
|
+
|
|
26
|
+
Configuration is read from the environment and a .env in the current
|
|
27
|
+
directory (see .env.example in the package). Requires a local model server
|
|
28
|
+
compatible with pi (e.g. llama-server on :5000) unless PIPELINE_ALLOW_CLOUD=1.
|
|
29
|
+
The terminal client lives in the same repo: packages/tui (bin: pmoe).`)
|
|
30
|
+
process.exit(0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (cmd !== "serve") {
|
|
34
|
+
console.error(`Unknown command: ${cmd} (try --help)`)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { register } = await import("tsx/esm/api")
|
|
39
|
+
register()
|
|
40
|
+
await import(resolve(root, "src", "server.ts"))
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pipeline-moe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Local-first multi-agent chat room — N stateful pi agent sessions over a shared workspace, with terminal and web clients.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/DAXZEIT/pipeline-moe.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"multi-agent",
|
|
13
|
+
"orchestration",
|
|
14
|
+
"llm",
|
|
15
|
+
"local-llm",
|
|
16
|
+
"llama-server",
|
|
17
|
+
"agents",
|
|
18
|
+
"tui",
|
|
19
|
+
"sse"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.12"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"pipeline-moe": "./bin/pipeline-moe.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"src",
|
|
30
|
+
"!src/__tests__",
|
|
31
|
+
"presets",
|
|
32
|
+
"web/dist",
|
|
33
|
+
".env.example",
|
|
34
|
+
"tsconfig.json"
|
|
35
|
+
],
|
|
36
|
+
"workspaces": [
|
|
37
|
+
"packages/*"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"predev": "bash scripts/predev.sh",
|
|
41
|
+
"dev": "concurrently -n api,web -c blue,green \"npm run dev:backend\" \"npm run dev:frontend\"",
|
|
42
|
+
"dev:backend": "tsx watch src/server.ts",
|
|
43
|
+
"dev:frontend": "npm -C web run dev",
|
|
44
|
+
"start": "tsx src/server.ts",
|
|
45
|
+
"start:full": "bash start.sh",
|
|
46
|
+
"build:web": "npm -C web run build",
|
|
47
|
+
"prepack": "npm -C web run build",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.79.4",
|
|
53
|
+
"cors": "^2.8.5",
|
|
54
|
+
"express": "^4.19.2",
|
|
55
|
+
"express-rate-limit": "^8.5.2",
|
|
56
|
+
"tsx": "^4.19.0",
|
|
57
|
+
"typebox": "^1.2.16",
|
|
58
|
+
"youtube-transcript-plus": "^2.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/cors": "^2.8.17",
|
|
62
|
+
"@types/express": "^4.17.21",
|
|
63
|
+
"@types/node": "^22.0.0",
|
|
64
|
+
"concurrently": "^10.0.3",
|
|
65
|
+
"typescript": "^5.6.0",
|
|
66
|
+
"vitest": "^4.1.9"
|
|
67
|
+
}
|
|
68
|
+
}
|