omnirush 0.8.5 → 0.9.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 (49) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/collect-once.ts +6 -5
  3. package/assets/extensions/omnirush/agents-lib.ts +196 -12
  4. package/assets/extensions/omnirush/agents.ts +97 -105
  5. package/assets/extensions/omnirush/auth.js +399 -115
  6. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  7. package/assets/extensions/omnirush/bgshell.ts +392 -0
  8. package/assets/extensions/omnirush/collector.ts +103 -16
  9. package/assets/extensions/omnirush/commands.ts +14 -15
  10. package/assets/extensions/omnirush/deliveries.ts +145 -0
  11. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  12. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  13. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  14. package/assets/extensions/omnirush/guard.ts +340 -0
  15. package/assets/extensions/omnirush/index.ts +12 -0
  16. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  17. package/assets/extensions/omnirush/sota.ts +104 -47
  18. package/assets/extensions/omnirush/status-lib.ts +3 -0
  19. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  20. package/assets/extensions/omnirush/subagents.ts +305 -0
  21. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  22. package/assets/extensions/omnirush/swarm.ts +95 -0
  23. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  24. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  25. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  26. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  27. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  28. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  29. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  30. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  31. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  32. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  33. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  34. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  35. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  36. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  37. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  38. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  39. package/assets/extensions/omnirush/voice.ts +838 -0
  40. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  41. package/assets/extensions/omnirush/yolo.ts +85 -0
  42. package/package.json +6 -2
  43. package/scripts/brand-engine.js +526 -0
  44. package/scripts/build-all-packages.py +29 -1
  45. package/scripts/smoke-packages.py +32 -1
  46. package/src/bin.js +252 -53
  47. package/src/compat.js +272 -0
  48. package/src/lib.js +64 -0
  49. package/scripts/patch-pi-branding.js +0 -251
@@ -36,10 +36,13 @@ import { StringEnum } from "@earendil-works/pi-ai";
36
36
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
37
37
  import { Type } from "typebox";
38
38
 
39
+ import { deliveryHub } from "./deliveries";
40
+ import { subagentTasks } from "./subagents";
39
41
  import {
40
42
  AGENT_ROLES,
41
43
  AGENTS_HOOK,
42
44
  AgentManager,
45
+ agentStatusSummary,
43
46
  type AgentRunner,
44
47
  type AgentsHook,
45
48
  type AgentRecord,
@@ -48,9 +51,11 @@ import {
48
51
  renderChildResults,
49
52
  renderDelivery,
50
53
  runChildAgent,
54
+ childModelFallback,
51
55
  type ChildResult,
52
56
  type ChildTask,
53
57
  } from "./agents-lib";
58
+ import { SWARM_GUIDELINE } from "./swarm-lib";
54
59
 
55
60
  /** A timeout_minutes below this is taken for a placeholder, not a limit. */
56
61
  const MIN_TIMEOUT_MINUTES = 1;
@@ -66,7 +71,7 @@ const SpawnAgentsParams = Type.Object({
66
71
  }),
67
72
  task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
68
73
  model: Type.Optional(Type.String({
69
- description: 'Cross-model child: gateway model id for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "meta-muse-spark", "muse-spark-1.1", "gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol"). Omit (or leave empty) to inherit this session\'s model',
74
+ description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "meta-muse-spark", "muse-spark-1.1:low", "gpt-6-astra", "gpt-6-sol:high", "gpt-5.6-sol"). Omit (or leave empty) for the sub-agent model the user picked with /subagents, else the parent session\'s current model and effort',
70
75
  })),
71
76
  }),
72
77
  { description: "Tasks to delegate; they all run in parallel", minItems: 1 },
@@ -99,45 +104,35 @@ function renderSome(records: AgentRecord[], unknown: string[] = []): string {
99
104
  const open = records.filter((record) => !record.result);
100
105
  const parts: string[] = [];
101
106
  if (finished.length > 0) parts.push(renderChildResults(finished.map((record) => record.result as ChildResult), finished.map((record) => record.id)));
102
- if (open.length > 0) parts.push(`Still running:\n${renderAgentStatus(open)}`);
107
+ if (open.length > 0) parts.push(`Open sub-agents:\n${renderAgentStatus(open)}`);
103
108
  if (unknown.length > 0) parts.push(`Unknown agent ids: ${unknown.join(", ")}`);
104
109
  return parts.join("\n\n") || "No matching sub-agents.";
105
110
  }
106
111
 
107
112
  export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {}) {
108
113
  const watchdogMinutes = Math.round(childInactivityMs() / 60_000);
109
- /** The session this runtime is on (deliveries go to it). */
110
- let currentSession = "";
111
- let cwd = process.cwd();
112
- /** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
113
- let busy = false;
114
- let oneShot = false;
114
+ const hub = deliveryHub(pi);
115
+ /** The last context seen (the TUI status bar for running agents). */
116
+ let lastContext: any = null;
117
+ let statusTimer: ReturnType<typeof setTimeout> | null = null;
115
118
 
116
- const send = (records: AgentRecord[]) => {
117
- pi.sendMessage(
118
- {
119
- customType: DELIVERY_TYPE,
120
- content: renderDelivery(records),
121
- display: true,
122
- details: { results: records.map(resultOf) },
123
- },
124
- // Busy: into the running turn at its next step. Idle: a new turn.
125
- { triggerTurn: true, deliverAs: "steer" },
126
- );
127
- };
119
+ function publishStatus(): void {
120
+ statusTimer = null;
121
+ const session = hub.currentSession;
122
+ if (!session || !lastContext?.ui?.setStatus) return;
123
+ const records = manager.list(session);
124
+ if (records.length > 0) lastContext.ui.setStatus("omnirush-agents", agentStatusSummary(records));
125
+ }
128
126
 
129
- const deliver = () => {
130
- if (!currentSession) return;
131
- // One-shot and between runs: the agent_settled loop delivers.
132
- if (oneShot && !busy) {
133
- return;
134
- }
135
- for (const group of manager.takeDeliverable(currentSession)) send(group.records);
136
- };
127
+ function scheduleStatus(): void {
128
+ if (statusTimer) return;
129
+ statusTimer = setTimeout(publishStatus, 250);
130
+ statusTimer.unref?.();
131
+ }
137
132
 
138
133
  const manager = new AgentManager({
139
134
  run: options.run ?? ((task, options) => runChildAgent(task, {
140
- cwd: options.cwd ?? cwd,
135
+ cwd: options.cwd ?? hub.cwd,
141
136
  parentSessionId: options.parentSessionId,
142
137
  sessionId: options.sessionId,
143
138
  signal: options.signal,
@@ -145,7 +140,19 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
145
140
  ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
146
141
  })),
147
142
  // After the settle's own bookkeeping (waiters took theirs).
148
- onDeliverable: () => queueMicrotask(deliver),
143
+ onDeliverable: () => hub.notify(),
144
+ onChanged: scheduleStatus,
145
+ });
146
+
147
+ hub.addSource({
148
+ hasPending: (session) => manager.hasPending(session),
149
+ // One message per finished batch (or per child with notify "each").
150
+ take: (session) => manager.takeDeliverable(session).map(({ records }) => ({
151
+ customType: DELIVERY_TYPE,
152
+ content: renderDelivery(records),
153
+ details: { results: records.map(resultOf) },
154
+ })),
155
+ changed: () => manager.changed(),
149
156
  });
150
157
 
151
158
  const hook: AgentsHook = {
@@ -157,11 +164,8 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
157
164
  (globalThis as any)[AGENTS_HOOK] = hook;
158
165
 
159
166
  const sessionOf = (ctx: any): string => {
160
- const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
161
- if (id) currentSession = id;
162
- if (ctx?.cwd) cwd = String(ctx.cwd);
163
- if (ctx?.mode === "print" || ctx?.mode === "json") oneShot = true;
164
- return id;
167
+ lastContext = ctx ?? lastContext;
168
+ return hub.sessionOf(ctx);
165
169
  };
166
170
 
167
171
  pi.registerTool({
@@ -186,25 +190,33 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
186
190
  "Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
187
191
  "Long work (implementation chunks, builds, test suites, long research — anything that may take many minutes) while you have other things to do: spawn_agents with wait:false, then keep working; the results arrive on their own as a message, so do not poll agents_status in a loop. Call agents_wait only when you have nothing else to do and need the results before going on.",
188
192
  "Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
193
+ SWARM_GUIDELINE,
189
194
  ],
190
195
  parameters: SpawnAgentsParams,
191
196
 
192
197
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
198
+ const parentSessionId = sessionOf(ctx);
193
199
  const tasks: ChildTask[] = [];
194
200
  for (const raw of params.tasks ?? []) {
195
201
  if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
196
202
  throw new Error("invalid tasks: every entry needs a non-empty task string");
197
203
  }
198
- // An empty model (models fill optional fields) inherits the session's.
199
- const model = typeof raw.model === "string" ? raw.model.trim() : "";
204
+ // A task's own model[:effort] wins; an omitted (or empty) one takes
205
+ // the /subagents pick, else the parent session's current model and
206
+ // effort (resolved below by subagentTasks).
207
+ const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
200
208
  tasks.push({
201
209
  role: raw.role,
202
210
  task: raw.task.trim(),
203
- ...(model ? { model } : {}),
211
+ ...(requestedModel ? { model: requestedModel } : {}),
204
212
  });
205
213
  }
206
214
  if (tasks.length === 0) throw new Error("no tasks given");
207
- const parentSessionId = sessionOf(ctx);
215
+ // The model and effort each child runs on (subagents.ts): the task's
216
+ // own model[:effort], else the /subagents pick, else the parent
217
+ // session's current model and effort; the main model when a pick is
218
+ // unavailable.
219
+ const resolvedTasks = subagentTasks(pi, ctx, tasks);
208
220
  if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
209
221
  const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
210
222
  // Only a deliberate limit counts: models fill optional numbers with
@@ -212,32 +224,42 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
212
224
  const timeoutMs = typeof params.timeout_minutes === "number" && params.timeout_minutes >= MIN_TIMEOUT_MINUTES
213
225
  ? params.timeout_minutes * 60_000
214
226
  : undefined;
215
- // No cap: every task runs at once unless the caller throttles.
216
- const concurrency = typeof params.max_parallel === "number" && params.max_parallel >= 1
227
+ // No cap: every task runs at once unless the caller deliberately
228
+ // supplies max_parallel.
229
+ const hasParallelLimit = typeof params.max_parallel === "number" && params.max_parallel >= 1;
230
+ const concurrency = hasParallelLimit
217
231
  ? Math.floor(params.max_parallel)
218
- : tasks.length;
232
+ : undefined;
219
233
  const background = params.wait === false;
220
234
 
221
- let settledCount = 0;
222
- const dispatched = manager.dispatch(parentSessionId, tasks, {
235
+ const dispatched = manager.dispatch(parentSessionId, resolvedTasks, {
223
236
  background,
224
237
  concurrency,
225
238
  notify: params.notify === "each" ? "each" : "batch",
226
239
  ...(background ? {} : { signal }),
227
240
  onSettled: (record) => {
228
- settledCount += 1;
229
241
  if (background) return;
230
242
  onUpdate?.({
231
- content: [{ type: "text", text: `spawn_agents: ${settledCount}/${tasks.length} settled (latest: ${record.id} ${record.role})` }],
243
+ content: [{ type: "text", text: `${agentStatusSummary(dispatched.records)} (latest: ${record.id} ${record.role})` }],
232
244
  details: { results: dispatched.records.filter((candidate) => candidate.result).map(resultOf) },
233
245
  });
234
246
  },
247
+ onChanged: () => {
248
+ if (background) return;
249
+ onUpdate?.({
250
+ content: [{ type: "text", text: agentStatusSummary(manager.list(parentSessionId)) }],
251
+ details: { results: manager.list(parentSessionId).filter((candidate) => candidate.result).map(resultOf) },
252
+ });
253
+ },
235
254
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
236
255
  cwd: workdir,
237
256
  });
238
257
 
239
258
  if (background) {
240
- const lines = dispatched.records.map((record) => `- ${record.id}: ${record.role}${record.model ? ` [${record.model}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}`);
259
+ const lines = dispatched.records.map((record, index) => {
260
+ const note = childModelFallback(resolvedTasks[index])?.note;
261
+ return `- ${record.id}: ${record.role}${record.model ? ` [${record.model}${record.effort ? ` · ${record.effort}` : ""}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}${note ? ` (${note})` : ""}`;
262
+ });
241
263
  return {
242
264
  content: [{
243
265
  type: "text",
@@ -254,14 +276,20 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
254
276
  details: {
255
277
  background: true,
256
278
  batch: dispatched.batch,
257
- results: dispatched.records.map((record) => ({
258
- agent_id: record.id,
259
- role: record.role,
260
- task: record.task,
261
- session_id: record.sessionId,
262
- ...(record.model ? { model: record.model } : {}),
263
- status: "running",
264
- })),
279
+ results: dispatched.records.map((record, index) => {
280
+ const modelFallback = childModelFallback(resolvedTasks[index]);
281
+ return {
282
+ agent_id: record.id,
283
+ role: record.role,
284
+ task: record.task,
285
+ session_id: record.sessionId,
286
+ ...(record.model ? { model: record.model } : {}),
287
+ ...(record.effort ? { effort: record.effort } : {}),
288
+ ...(modelFallback ? { model_fallback: modelFallback } : {}),
289
+ parallel_limit: record.parallelLimit,
290
+ status: "running",
291
+ };
292
+ }),
265
293
  },
266
294
  };
267
295
  }
@@ -286,7 +314,20 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
286
314
  const text = renderAgentStatus(records) + (unknown.length ? `\nUnknown agent ids: ${unknown.join(", ")}` : "");
287
315
  return {
288
316
  content: [{ type: "text", text }],
289
- details: { agents: records.map((record) => ({ agent_id: record.id, status: record.status, role: record.role, session_id: record.sessionId })) },
317
+ details: {
318
+ agents: records.map((record) => ({
319
+ agent_id: record.id,
320
+ status: record.status,
321
+ role: record.role,
322
+ task: record.task,
323
+ session_id: record.sessionId,
324
+ ...(record.model ? { model: record.model } : {}),
325
+ parallel_limit: record.parallelLimit,
326
+ turns: record.turns,
327
+ tools_running: record.toolsRunning,
328
+ last_activity_at: record.lastActivityAt,
329
+ })),
330
+ },
290
331
  };
291
332
  },
292
333
  });
@@ -356,55 +397,6 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
356
397
  },
357
398
  });
358
399
 
359
- pi.on("session_start", (_event: any, ctx: any) => {
360
- sessionOf(ctx);
361
- });
362
-
363
- pi.on("agent_start", (_event: any, ctx: any) => {
364
- sessionOf(ctx);
365
- busy = true;
366
- });
367
-
368
- /**
369
- * One-shot runs (print/json mode) end when the agent settles: background
370
- * children still out would be lost. The run waits for them here and
371
- * continues with their results (a new run nested in this settle, whose
372
- * own settle resumes this one when everything is done).
373
- */
374
- let resumeOuter: (() => void) | null = null;
375
- pi.on("agent_settled", async (_event: any, ctx: any) => {
376
- busy = false;
377
- const session = sessionOf(ctx);
378
- if (!oneShot) {
379
- // Anything that finished right as the run ended.
380
- deliver();
381
- return;
382
- }
383
- const outer = resumeOuter;
384
- resumeOuter = null;
385
- try {
386
- for (;;) {
387
- if (typeof ctx?.isIdle === "function" && !ctx.isIdle()) {
388
- // A run started (a delivery): its settle resumes this one.
389
- await new Promise<void>((resolve) => { resumeOuter = resolve; });
390
- break;
391
- }
392
- if (!session || !manager.hasPending(session)) break;
393
- const groups = manager.takeDeliverable(session);
394
- if (groups.length === 0) {
395
- await manager.changed();
396
- continue;
397
- }
398
- busy = true;
399
- const records = groups.flatMap((group) => group.records);
400
- send(records);
401
- if (typeof ctx?.isIdle === "function" && ctx.isIdle()) busy = false;
402
- }
403
- } finally {
404
- outer?.();
405
- }
406
- });
407
-
408
400
  pi.on("session_shutdown", async () => {
409
401
  // Children the session ends under are stopped (the collector, when it
410
402
  // runs, already did this before its last capture).