omnirush 0.8.6 → 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 (47) hide show
  1. package/assets/CHANGELOG.md +85 -0
  2. package/assets/extensions/omnirush/agents-lib.ts +134 -13
  3. package/assets/extensions/omnirush/agents.ts +51 -107
  4. package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
  5. package/assets/extensions/omnirush/bgshell.ts +392 -0
  6. package/assets/extensions/omnirush/collector.ts +72 -11
  7. package/assets/extensions/omnirush/commands.ts +2 -0
  8. package/assets/extensions/omnirush/deliveries.ts +145 -0
  9. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  10. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  11. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  12. package/assets/extensions/omnirush/guard.ts +340 -0
  13. package/assets/extensions/omnirush/index.ts +12 -0
  14. package/assets/extensions/omnirush/pi-engine.ts +65 -1
  15. package/assets/extensions/omnirush/sota.ts +52 -4
  16. package/assets/extensions/omnirush/status-lib.ts +3 -0
  17. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  18. package/assets/extensions/omnirush/subagents.ts +305 -0
  19. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  20. package/assets/extensions/omnirush/swarm.ts +95 -0
  21. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  22. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  23. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  24. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  25. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  26. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  27. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  28. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  29. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  30. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  31. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  32. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  33. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  34. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  35. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  36. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  37. package/assets/extensions/omnirush/voice.ts +838 -0
  38. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  39. package/assets/extensions/omnirush/yolo.ts +85 -0
  40. package/package.json +7 -3
  41. package/scripts/brand-engine.js +526 -0
  42. package/scripts/build-all-packages.py +29 -1
  43. package/scripts/smoke-packages.py +32 -1
  44. package/src/bin.js +205 -33
  45. package/src/compat.js +272 -0
  46. package/src/lib.js +64 -0
  47. package/scripts/patch-pi-branding.js +0 -251
@@ -33,6 +33,10 @@
33
33
  // a command the user ran)
34
34
  // custom / branch summary -> user message with synthetic text parts (what
35
35
  // the model was shown; never a turn opener)
36
+ // background command -> the delivery message as above, answered by an
37
+ // results (bgshell.ts) assistant message holding one bash tool part
38
+ // per finished command (its output and exit
39
+ // code; no model step, no tokens)
36
40
  // compaction -> user message with a compaction part and the
37
41
  // summary as an assistant `summary` message
38
42
  // system prompt -> not a message (the desktop records it in
@@ -49,6 +53,9 @@
49
53
  import { createHash } from "node:crypto";
50
54
  import { isAbsolute, resolve } from "node:path";
51
55
 
56
+ /** customType of bgshell.ts's finished-background-command message. */
57
+ export const BACKGROUND_BASH_TYPE = "omnirush-bash-result";
58
+
52
59
  export type EngineMessage = { info: Record<string, unknown>; parts: Array<Record<string, unknown>> };
53
60
 
54
61
  /** One pi session entry (loosely typed: the files are parsed without validation). */
@@ -295,6 +302,60 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
295
302
  return messageVariant ?? null;
296
303
  };
297
304
 
305
+ /**
306
+ * Finished background shell commands (bgshell.ts delivery): their output
307
+ * as bash tool parts, the way the engine records a command the user ran.
308
+ */
309
+ const backgroundBash = (parentId: string, entryId: string, details: unknown, at: number | null, index: number): void => {
310
+ const jobs = isRecord(details) && Array.isArray(details.jobs) ? details.jobs.filter(isRecord) : [];
311
+ if (jobs.length === 0) return;
312
+ const shellId = engineMessageId(entryId, "_bash");
313
+ const shellModel = model ?? nextAssistantModel.get(index) ?? null;
314
+ const time = at ?? 0;
315
+ const parts: Array<Record<string, unknown>> = jobs.map((job, jobIndex) => {
316
+ const start = typeof job.started_at === "number" ? job.started_at : time;
317
+ const end = typeof job.finished_at === "number" ? job.finished_at : time;
318
+ const failed = job.status === "failed";
319
+ return {
320
+ type: "tool",
321
+ callID: `bg_${typeof job.id === "string" ? job.id : jobIndex}_${entryId}`,
322
+ tool: "bash",
323
+ state: {
324
+ status: failed ? "error" : "completed",
325
+ input: { command: typeof job.command === "string" ? job.command : "", background_id: job.id ?? null },
326
+ ...(failed ? { error: typeof job.error === "string" ? job.error : "failed" } : {}),
327
+ output: typeof job.output === "string" ? job.output : "",
328
+ metadata: {
329
+ background: true,
330
+ ...(typeof job.exit_code === "number" ? { exit: job.exit_code } : {}),
331
+ ...(typeof job.status === "string" ? { status: job.status } : {}),
332
+ ...(job.interrupted === true ? { interrupted: true } : {}),
333
+ ...(job.truncated === true ? { truncated: true } : {}),
334
+ ...(typeof job.tool_call_id === "string" ? { startedBy: job.tool_call_id } : {}),
335
+ },
336
+ time: { start, end },
337
+ },
338
+ };
339
+ });
340
+ out.push({
341
+ info: {
342
+ id: shellId,
343
+ sessionID: sessionId,
344
+ role: "assistant",
345
+ parentID: parentId,
346
+ time: { created: time, completed: time },
347
+ ...(shellModel ? { modelID: shellModel.modelID, providerID: shellModel.providerID } : {}),
348
+ agent,
349
+ mode: agent,
350
+ path: { cwd, root: cwd },
351
+ cost: 0,
352
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
353
+ finish: "stop",
354
+ },
355
+ parts: parts.map((part, partIndex) => ({ id: partId(shellId, partIndex), sessionID: sessionId, messageID: shellId, ...part })),
356
+ });
357
+ };
358
+
298
359
  entries.forEach((entry, index) => {
299
360
  const entryId = typeof entry.id === "string" && entry.id ? entry.id : null;
300
361
  if (!entryId) return;
@@ -340,7 +401,9 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
340
401
  }
341
402
  if (entry.type === "custom_message") {
342
403
  const parts = contentParts(entry.content, { synthetic: true, metadata: { customType: entry.customType ?? null } });
343
- if (parts.length > 0) userMessage(engineMessageId(entryId), at, parts, index);
404
+ const id = engineMessageId(entryId);
405
+ if (parts.length > 0) userMessage(id, at, parts, index);
406
+ if (entry.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, (entry as any).details, at, index);
344
407
  return;
345
408
  }
346
409
  if (entry.type !== "message" || !isRecord(entry.message)) return;
@@ -356,6 +419,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
356
419
  case "custom": {
357
420
  const parts = contentParts(message.content, { synthetic: true, metadata: { customType: message.customType ?? null } });
358
421
  if (parts.length > 0) userMessage(id, created, parts, index);
422
+ if (message.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, message.details, created, index);
359
423
  return;
360
424
  }
361
425
  case "branchSummary":
@@ -41,6 +41,14 @@ import {
41
41
  streamIdleMs,
42
42
  withStallWatchdog,
43
43
  } from "./sota-lib";
44
+ import { omniDir } from "./auth";
45
+ import {
46
+ ENV_FALLBACK_EFFORT,
47
+ ENV_FALLBACK_MODEL,
48
+ FALLBACK_MARKER,
49
+ subagentFallbackFetch,
50
+ type FallbackRoute,
51
+ } from "./subagents-lib";
44
52
  import {
45
53
  formatUnavailableSummary,
46
54
  isRetryableStatus,
@@ -222,7 +230,7 @@ async function readErrorBody(response: any): Promise<string> {
222
230
  */
223
231
  function tapFetch(
224
232
  baseFetch: any,
225
- expectedModelId: string,
233
+ expectedModel: string | (() => string),
226
234
  onViolation: (expected: string, served: string) => void,
227
235
  idleMs: number,
228
236
  ): any {
@@ -356,6 +364,8 @@ function tapFetch(
356
364
  const onData = (data: string) => {
357
365
  if (reported) return;
358
366
  const served = servedModelFromSseData(data);
367
+ // A sub-agent moved to the main model expects that one.
368
+ const expectedModelId = typeof expectedModel === "function" ? expectedModel() : expectedModel;
359
369
  if (served === undefined || !isSotaViolation(expectedModelId, served)) return;
360
370
  reported = true;
361
371
  onViolation(expectedModelId, served);
@@ -390,22 +400,60 @@ export default function (pi: any) {
390
400
  const responses = openAIResponsesApi();
391
401
  const strictSota = (): boolean => pi.getFlag("strict-sota") === true;
392
402
 
403
+ // A sub-agent on a picked model: the main model it falls back to when the
404
+ // gateway refuses the picked one (set by the parent's spawn_agents).
405
+ const fallbackModel = (process.env[ENV_FALLBACK_MODEL] || "").trim();
406
+ const fallbackEffort = (process.env[ENV_FALLBACK_EFFORT] || "").trim() || null;
407
+
393
408
  const streamWith =
394
409
  (simple: boolean) =>
395
410
  (model: any, context: any, options: any) => {
396
- const baseFetch = options?.fetch ?? globalThis.fetch;
411
+ let baseFetch = options?.fetch ?? globalThis.fetch;
412
+ const route: FallbackRoute = { expected: model.id, movedTo: null };
413
+ if (fallbackModel && fallbackModel !== model.id) {
414
+ baseFetch = subagentFallbackFetch(baseFetch, {
415
+ model: fallbackModel,
416
+ effort: fallbackEffort,
417
+ dir: omniDir(),
418
+ route,
419
+ report: (event) => {
420
+ // The parent's spawn_agents reads this line from the child's stderr.
421
+ try {
422
+ process.stderr.write(`\n${FALLBACK_MARKER}${JSON.stringify(event)}\n`);
423
+ } catch {
424
+ /* stderr may be closed */
425
+ }
426
+ },
427
+ });
428
+ }
397
429
  const tapped = {
398
430
  ...(options ?? {}),
399
431
  fetch: tapFetch(
400
432
  baseFetch,
401
- model.id,
433
+ () => route.expected,
402
434
  (expected, served) => reportViolation(strictSota(), expected, served),
403
435
  streamIdleMs(process.env.OMNIRUSH_STREAM_IDLE_MS, options?.timeoutMs),
404
436
  ),
405
437
  };
406
- return simple
438
+ const stream = simple
407
439
  ? responses.streamSimple(model, context, tapped)
408
440
  : responses.stream(model, context, tapped);
441
+ if (fallbackModel && stream && typeof stream.push === "function") {
442
+ // The message records the model (and effort) that really answered.
443
+ const push = stream.push.bind(stream);
444
+ stream.push = (event: any) => {
445
+ if (route.movedTo) {
446
+ for (const message of [event?.partial, event?.message, event?.error]) {
447
+ if (message && typeof message === "object" && message.role === "assistant") {
448
+ message.model = route.movedTo.model;
449
+ if (route.movedTo.effort) message.providerThinkingLevel = route.movedTo.effort;
450
+ }
451
+ }
452
+ }
453
+ return push(event);
454
+ };
455
+ }
456
+ return stream;
409
457
  };
410
458
 
411
459
  pi.registerProvider({
@@ -52,6 +52,8 @@ export interface StatusInput {
52
52
  origin: string;
53
53
  /** omnirush CLI version ("unknown" when not provided by the launcher). */
54
54
  version: string;
55
+ /** The approval mode line (yolo-lib formatModeStatus), when known. */
56
+ yolo?: string | null;
55
57
  }
56
58
 
57
59
  /**
@@ -79,6 +81,7 @@ export function formatStatusLines(input: StatusInput): string[] {
79
81
  }
80
82
 
81
83
  lines.push(formatCollectorLine(input.collector));
84
+ if (input.yolo) lines.push(input.yolo);
82
85
  lines.push(`manager: ${input.origin}`);
83
86
  lines.push(`version: ${input.version}`);
84
87
  return lines;