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.
Files changed (48) hide show
  1. package/.env.example +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +408 -0
  4. package/bin/pipeline-moe.mjs +40 -0
  5. package/package.json +68 -0
  6. package/presets/2106BUILD.json +163 -0
  7. package/presets/CHEAPBUILD.json +97 -0
  8. package/presets/FREEROOM.json +96 -0
  9. package/presets/Versa.json +145 -0
  10. package/presets/cloud-main.json +76 -0
  11. package/presets/cloud-sprint.json +70 -0
  12. package/presets/local-default.json +152 -0
  13. package/presets/main.json +147 -0
  14. package/presets/mainmix.json +145 -0
  15. package/src/circuit-breaker.ts +141 -0
  16. package/src/config.ts +46 -0
  17. package/src/custom-tools/arxiv-search.ts +186 -0
  18. package/src/custom-tools/check-room.ts +45 -0
  19. package/src/custom-tools/destroy-room.ts +45 -0
  20. package/src/custom-tools/index.ts +75 -0
  21. package/src/custom-tools/spawn-room.ts +99 -0
  22. package/src/custom-tools/stop-room.ts +50 -0
  23. package/src/custom-tools/web-read.ts +104 -0
  24. package/src/custom-tools/web-search.ts +144 -0
  25. package/src/custom-tools/youcom-search.ts +230 -0
  26. package/src/custom-tools/youtube-transcript.ts +124 -0
  27. package/src/local-model-lock.ts +52 -0
  28. package/src/model.ts +131 -0
  29. package/src/orchestrator.ts +59 -0
  30. package/src/participant.ts +423 -0
  31. package/src/path-guard.ts +13 -0
  32. package/src/personas.ts +480 -0
  33. package/src/receipts.ts +120 -0
  34. package/src/registry.ts +317 -0
  35. package/src/room-manager.ts +505 -0
  36. package/src/room.ts +1657 -0
  37. package/src/sandbox-tools.ts +141 -0
  38. package/src/server.ts +1846 -0
  39. package/src/sse.ts +86 -0
  40. package/src/sshfs.ts +139 -0
  41. package/src/store.ts +79 -0
  42. package/src/types.ts +131 -0
  43. package/src/validation.ts +36 -0
  44. package/tsconfig.json +15 -0
  45. package/web/dist/assets/index-CmMGhMKG.css +1 -0
  46. package/web/dist/assets/index-LAGqbZII.js +41 -0
  47. package/web/dist/index.html +13 -0
  48. 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
+ }