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.

Potentially problematic release.


This version of pipeline-moe might be problematic. Click here for more details.

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
@@ -0,0 +1,317 @@
1
+ // The participant registry: the live roster of agents. Create / activate /
2
+ // deactivate / kick all happen here. Emits a "roster" SSE event on any change.
3
+
4
+ import { config } from "./config.js"
5
+ import { isAllowedModel as isAllowedModel_ } from "./model.js"
6
+ import { Participant } from "./participant.js"
7
+ import type { ResolvedModel } from "./model.js"
8
+ import type { RoomOrchestrator } from "./orchestrator.js"
9
+ import type { SseHub } from "./sse.js"
10
+ import type { Persona, PersonaState } from "./types.js"
11
+
12
+ export interface RosterItem {
13
+ id: string
14
+ name: string
15
+ color: string
16
+ icon: string
17
+ tools: string[]
18
+ active: boolean
19
+ status: string
20
+ /** Per-agent model "provider/id", or undefined when on the default. */
21
+ model?: string
22
+ /** Per-agent thinking level, or undefined when inheriting from global config. */
23
+ thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
24
+ /** May run concurrently with adjacent parallel-flagged agents. */
25
+ parallel: boolean
26
+ }
27
+
28
+ export class Registry {
29
+ private participants = new Map<string, Participant>()
30
+ /** Fired after any roster mutation (create/activate/kick). Used for autosave. */
31
+ onChange: (() => void) | null = null
32
+
33
+ constructor(
34
+ private readonly resolved: ResolvedModel,
35
+ private readonly hub: SseHub,
36
+ /** Providers the user has explicitly enabled via /api/providers. */
37
+ private readonly explicitlyEnabledProviders: Set<string> = new Set(),
38
+ /** Directory each participant's file tools are confined to. Defaults to the
39
+ * pipeline workspace; per-room scopes override it. */
40
+ private readonly workspaceDir: string = config.workspaceDir,
41
+ /** Logical room this registry belongs to. Tags roster + participant SSE
42
+ * events so room-filtered clients don't receive another room's roster. */
43
+ private readonly roomId: string = "default",
44
+ /** Capability surface for spawning sub-rooms. Passed to each participant so
45
+ * orchestrator personas (the planner) get spawn/check/destroy_room tools.
46
+ * Undefined in tests and before the server wires it up. */
47
+ private readonly orchestrator?: RoomOrchestrator,
48
+ ) {}
49
+
50
+ /** Default thinking level for new participants without a per-agent override.
51
+ * Mutable — the Room can change it at runtime and existing participants
52
+ * keep their current level; only new participants pick up the change. */
53
+ private defaultThinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" = config.thinkingLevel
54
+
55
+ setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void {
56
+ this.defaultThinkingLevel = level
57
+ }
58
+
59
+ getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" {
60
+ return this.defaultThinkingLevel
61
+ }
62
+
63
+ /** Whether cloud models are allowed in this room. Mutable — the Room can
64
+ * change it at runtime; only new participants pick up the change. */
65
+ private allowCloud: boolean = config.allowCloud
66
+
67
+ setAllowCloud(value: boolean): void {
68
+ this.allowCloud = value
69
+ }
70
+
71
+ getAllowCloud(): boolean {
72
+ return this.allowCloud
73
+ }
74
+
75
+ /** Reserve tokens for auto-compaction. Mutable — the Room can change it at
76
+ * runtime; only new participants pick up the change. */
77
+ private compactionReserveTokens: number = 38000
78
+
79
+ setCompactionReserveTokens(value: number): void {
80
+ this.compactionReserveTokens = value
81
+ }
82
+
83
+ getCompactionReserveTokens(): number {
84
+ return this.compactionReserveTokens
85
+ }
86
+
87
+ /** Participants that should take part in the loop, in insertion order. */
88
+ activeParticipants(): Participant[] {
89
+ return [...this.participants.values()].filter((p) => p.active)
90
+ }
91
+
92
+ /** Snapshot of the roster as personas + runtime flags, for persistence. */
93
+ personaStates(): PersonaState[] {
94
+ return [...this.participants.values()].map((p) => ({
95
+ ...p.persona,
96
+ active: p.active,
97
+ parallel: p.parallel,
98
+ }))
99
+ }
100
+
101
+ get(id: string): Participant | undefined {
102
+ return this.participants.get(id)
103
+ }
104
+
105
+ has(id: string): boolean {
106
+ return this.participants.has(id)
107
+ }
108
+
109
+ roster(): RosterItem[] {
110
+ return [...this.participants.values()].map((p) => ({
111
+ id: p.persona.id,
112
+ name: p.persona.name,
113
+ color: p.persona.color,
114
+ icon: p.persona.icon,
115
+ tools: p.persona.tools,
116
+ active: p.active,
117
+ status: p.status,
118
+ model: p.persona.model,
119
+ thinkingLevel: p.persona.thinkingLevel,
120
+ parallel: p.parallel,
121
+ }))
122
+ }
123
+
124
+ broadcastRoster(): void {
125
+ this.hub.broadcast("roster", this.roster(), this.roomId)
126
+ }
127
+
128
+ async create(persona: Persona, active = true, parallel = false): Promise<Participant> {
129
+ if (this.participants.has(persona.id)) {
130
+ throw new Error(`participant "${persona.id}" already exists`)
131
+ }
132
+ console.log(`[Registry.create] persona=${persona.id} has orchestrator=${!!this.orchestrator}`)
133
+ const participant = await Participant.create(
134
+ persona,
135
+ this.resolved,
136
+ (event, data) => this.hub.broadcast(event, data, this.roomId),
137
+ this.workspaceDir,
138
+ this.orchestrator,
139
+ this.defaultThinkingLevel,
140
+ this.allowCloud,
141
+ this.compactionReserveTokens,
142
+ )
143
+ // Catch a new participant up on everything said so far before its first turn.
144
+ participant.cursor = 0
145
+ participant.active = active
146
+ participant.parallel = parallel
147
+ this.participants.set(persona.id, participant)
148
+ this.broadcastRoster()
149
+ this.onChange?.()
150
+ return participant
151
+ }
152
+
153
+ /** Editable persona fields (id is immutable — it is the @mention handle). */
154
+ async update(
155
+ id: string,
156
+ patch: Partial<Pick<Persona, "name" | "color" | "icon" | "tools" | "systemPrompt" | "model" | "thinkingLevel">>,
157
+ ): Promise<Participant> {
158
+ const existing = this.participants.get(id)
159
+ if (!existing) throw new Error(`unknown participant "${id}"`)
160
+ const order = [...this.participants.keys()]
161
+ const wasActive = existing.active
162
+ const persona: Persona = { ...existing.persona, ...patch, id }
163
+
164
+ // A new system prompt = a new identity, so we rebuild the pi session now
165
+ // rather than mutating the running one. cursor=0 → it replays the room
166
+ // transcript on its next turn with the new persona.
167
+ existing.dispose()
168
+ const replacement = await Participant.create(
169
+ persona,
170
+ this.resolved,
171
+ (event, data) => this.hub.broadcast(event, data, this.roomId),
172
+ this.workspaceDir,
173
+ this.orchestrator,
174
+ this.defaultThinkingLevel,
175
+ this.allowCloud,
176
+ this.compactionReserveTokens,
177
+ )
178
+ replacement.cursor = 0
179
+ replacement.active = wasActive
180
+
181
+ // Rebuild the Map in the original order (a plain set() would move it last,
182
+ // reordering @all / first-active routing).
183
+ const rebuilt = new Map<string, Participant>()
184
+ for (const key of order) {
185
+ rebuilt.set(key, key === id ? replacement : this.participants.get(key)!)
186
+ }
187
+ this.participants = rebuilt
188
+
189
+ this.broadcastRoster()
190
+ this.onChange?.()
191
+ return replacement
192
+ }
193
+
194
+ /** Reorder the roster to match the given id sequence. This is the first-turn /
195
+ * @all execution order (and the first-active fallback default). Ids not listed
196
+ * keep their relative order, appended after the listed ones. No session is
197
+ * recreated — only references move — so it is safe to call mid-turn. */
198
+ reorder(orderedIds: string[]): void {
199
+ const seen = new Set<string>()
200
+ const next: string[] = []
201
+ for (const id of orderedIds) {
202
+ if (this.participants.has(id) && !seen.has(id)) {
203
+ next.push(id)
204
+ seen.add(id)
205
+ }
206
+ }
207
+ for (const id of this.participants.keys()) if (!seen.has(id)) next.push(id)
208
+
209
+ const rebuilt = new Map<string, Participant>()
210
+ for (const id of next) rebuilt.set(id, this.participants.get(id)!)
211
+ this.participants = rebuilt
212
+
213
+ this.broadcastRoster()
214
+ this.onChange?.()
215
+ }
216
+
217
+ setActive(id: string, active: boolean): Participant {
218
+ const p = this.participants.get(id)
219
+ if (!p) throw new Error(`unknown participant "${id}"`)
220
+ p.active = active
221
+ p.status = "idle"
222
+ this.broadcastRoster()
223
+ this.onChange?.()
224
+ return p
225
+ }
226
+
227
+ /** Toggle whether an agent may run concurrently. Pure runtime flag — no
228
+ * session recreation, so it is safe to flip anytime (takes effect next turn). */
229
+ setParallel(id: string, parallel: boolean): Participant {
230
+ const p = this.participants.get(id)
231
+ if (!p) throw new Error(`unknown participant "${id}"`)
232
+ p.parallel = parallel
233
+ this.broadcastRoster()
234
+ this.onChange?.()
235
+ return p
236
+ }
237
+
238
+ /** In-place thinking level change — no session recreation. Takes effect next turn. */
239
+ async setThinkingLevel(
240
+ id: string,
241
+ level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh",
242
+ ): Promise<Participant> {
243
+ const p = this.participants.get(id)
244
+ if (!p) throw new Error(`unknown participant "${id}"`)
245
+ await p.setThinkingLevel(level)
246
+ this.broadcastRoster()
247
+ this.onChange?.()
248
+ return p
249
+ }
250
+
251
+ kick(id: string): void {
252
+ const p = this.participants.get(id)
253
+ if (!p) throw new Error(`unknown participant "${id}"`)
254
+ p.dispose()
255
+ this.participants.delete(id)
256
+ this.broadcastRoster()
257
+ this.onChange?.()
258
+ }
259
+
260
+ /** Replace the whole roster with a saved persona set (fresh pi sessions).
261
+ * Used when loading/switching conversations. Does not fire onChange. */
262
+ async reset(states: PersonaState[]): Promise<void> {
263
+ const cb = this.onChange
264
+ this.onChange = null // suppress per-participant autosave during a bulk swap
265
+ try {
266
+ for (const p of this.participants.values()) p.dispose()
267
+ this.participants.clear()
268
+ for (const s of states) {
269
+ const { active, parallel, ...persona } = s
270
+ await this.create(persona, active, parallel ?? false)
271
+ }
272
+ } finally {
273
+ this.onChange = cb
274
+ }
275
+ this.broadcastRoster()
276
+ }
277
+
278
+ disposeAll(): void {
279
+ for (const p of this.participants.values()) p.dispose()
280
+ this.participants.clear()
281
+ }
282
+
283
+ /** True if a "provider/id" ref is a model the UI is allowed to assign. */
284
+ isAllowedModel(ref: string): boolean {
285
+ return isAllowedModel_(this.resolved, this.allowCloud, ref, this.explicitlyEnabledProviders)
286
+ }
287
+
288
+ // ── Provider auth (for /provider slash command) ──────────────────────────
289
+
290
+ /** Get all providers with their auth status (no secrets). */
291
+ getProviderList(): Array<{ name: string; displayName: string; configured: boolean; models: number }> {
292
+ const all = this.resolved.modelRegistry.getAll()
293
+ const providerSet = new Set(all.map((m) => m.provider))
294
+ return Array.from(providerSet).map((name) => ({
295
+ name,
296
+ displayName: this.resolved.modelRegistry.getProviderDisplayName(name),
297
+ configured: this.resolved.modelRegistry.getProviderAuthStatus(name).configured,
298
+ models: all.filter((m) => m.provider === name).length,
299
+ }))
300
+ }
301
+
302
+ /** Set an API key for a provider (persisted). Returns auth status, not the key. */
303
+ setProviderKey(name: string, key: string): { name: string; configured: boolean } {
304
+ this.resolved.authStorage.set(name, { type: "api_key", key })
305
+ this.explicitlyEnabledProviders.add(name)
306
+ this.resolved.modelRegistry.refresh()
307
+ return { name, configured: this.resolved.modelRegistry.getProviderAuthStatus(name).configured }
308
+ }
309
+
310
+ /** Remove credentials for a provider. Returns auth status. */
311
+ removeProviderKey(name: string): { name: string; configured: boolean } {
312
+ this.resolved.authStorage.remove(name)
313
+ this.explicitlyEnabledProviders.delete(name)
314
+ this.resolved.modelRegistry.refresh()
315
+ return { name, configured: this.resolved.modelRegistry.getProviderAuthStatus(name).configured }
316
+ }
317
+ }