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.
- package/assets/CHANGELOG.md +85 -0
- package/assets/collect-once.ts +6 -5
- package/assets/extensions/omnirush/agents-lib.ts +196 -12
- package/assets/extensions/omnirush/agents.ts +97 -105
- package/assets/extensions/omnirush/auth.js +399 -115
- package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
- package/assets/extensions/omnirush/bgshell.ts +392 -0
- package/assets/extensions/omnirush/collector.ts +103 -16
- package/assets/extensions/omnirush/commands.ts +14 -15
- package/assets/extensions/omnirush/deliveries.ts +145 -0
- package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
- package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
- package/assets/extensions/omnirush/guard-lib.ts +230 -0
- package/assets/extensions/omnirush/guard.ts +340 -0
- package/assets/extensions/omnirush/index.ts +12 -0
- package/assets/extensions/omnirush/pi-engine.ts +65 -1
- package/assets/extensions/omnirush/sota.ts +104 -47
- package/assets/extensions/omnirush/status-lib.ts +3 -0
- package/assets/extensions/omnirush/subagents-lib.ts +623 -0
- package/assets/extensions/omnirush/subagents.ts +305 -0
- package/assets/extensions/omnirush/swarm-lib.ts +142 -0
- package/assets/extensions/omnirush/swarm.ts +95 -0
- package/assets/extensions/omnirush/voice/capture.ts +502 -0
- package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
- package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
- package/assets/extensions/omnirush/voice/core/index.ts +21 -0
- package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
- package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
- package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
- package/assets/extensions/omnirush/voice/core/session.ts +403 -0
- package/assets/extensions/omnirush/voice/core/text.ts +81 -0
- package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
- package/assets/extensions/omnirush/voice/core/types.ts +102 -0
- package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
- package/assets/extensions/omnirush/voice/keys.ts +435 -0
- package/assets/extensions/omnirush/voice/kitty.ts +64 -0
- package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
- package/assets/extensions/omnirush/voice/settings.ts +67 -0
- package/assets/extensions/omnirush/voice.ts +838 -0
- package/assets/extensions/omnirush/yolo-lib.ts +80 -0
- package/assets/extensions/omnirush/yolo.ts +85 -0
- package/package.json +6 -2
- package/scripts/brand-engine.js +526 -0
- package/scripts/build-all-packages.py +29 -1
- package/scripts/smoke-packages.py +32 -1
- package/src/bin.js +252 -53
- package/src/compat.js +272 -0
- package/src/lib.js +64 -0
- 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
|
-
|
|
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":
|
|
@@ -31,6 +31,7 @@ import fs from "node:fs";
|
|
|
31
31
|
import { openAIResponsesApi } from "@earendil-works/pi-ai";
|
|
32
32
|
import { recordGatewayUsage } from "./usage";
|
|
33
33
|
import { sharedRefresher } from "./refresh";
|
|
34
|
+
import { sendWithDeviceAuth, sessionEndedMessage } from "./auth";
|
|
34
35
|
import {
|
|
35
36
|
createSseDataScanner,
|
|
36
37
|
formatSotaWarning,
|
|
@@ -40,6 +41,14 @@ import {
|
|
|
40
41
|
streamIdleMs,
|
|
41
42
|
withStallWatchdog,
|
|
42
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";
|
|
43
52
|
import {
|
|
44
53
|
formatUnavailableSummary,
|
|
45
54
|
isRetryableStatus,
|
|
@@ -202,8 +211,9 @@ async function readErrorBody(response: any): Promise<string> {
|
|
|
202
211
|
|
|
203
212
|
/**
|
|
204
213
|
* Wrap a fetch implementation so that:
|
|
205
|
-
* -
|
|
206
|
-
*
|
|
214
|
+
* - every attempt carries the current on-disk device token, and a 401
|
|
215
|
+
* adopts a newer on-disk pair or refreshes under the cross-process
|
|
216
|
+
* lock, then resends (auth.js sendWithDeviceAuth),
|
|
207
217
|
* - 429 / 5xx / network failures retry with exponential backoff + full
|
|
208
218
|
* jitter (progress lines on stderr), and
|
|
209
219
|
* - SSE response bodies stream through a pass-through tap that observes
|
|
@@ -220,7 +230,7 @@ async function readErrorBody(response: any): Promise<string> {
|
|
|
220
230
|
*/
|
|
221
231
|
function tapFetch(
|
|
222
232
|
baseFetch: any,
|
|
223
|
-
|
|
233
|
+
expectedModel: string | (() => string),
|
|
224
234
|
onViolation: (expected: string, served: string) => void,
|
|
225
235
|
idleMs: number,
|
|
226
236
|
): any {
|
|
@@ -231,47 +241,46 @@ function tapFetch(
|
|
|
231
241
|
|
|
232
242
|
const outcome = await withRetries(
|
|
233
243
|
async (attempt, ref) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
/* usage capture must never break the request */
|
|
244
|
-
}
|
|
245
|
-
if (response?.status === 401) {
|
|
246
|
-
const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
|
|
247
|
-
debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
|
|
248
|
-
if (tokenUsed) {
|
|
249
|
-
const refresher = sharedRefresher();
|
|
250
|
-
const refreshed = await refresher.refresh(tokenUsed).catch((error: any) => {
|
|
251
|
-
debug(`refresh threw: ${error?.message ?? error}`);
|
|
252
|
-
return false;
|
|
253
|
-
});
|
|
254
|
-
const next = refresher.auth?.accessToken;
|
|
255
|
-
debug(`refresh ok: ${refreshed}, next token present: ${Boolean(next)}`);
|
|
256
|
-
if (refreshed && next) {
|
|
257
|
-
response = await doFetch(next);
|
|
258
|
-
debug(`retry-after-refresh status: ${response?.status}`);
|
|
259
|
-
try {
|
|
260
|
-
recordGatewayUsage(response?.headers);
|
|
261
|
-
} catch {
|
|
262
|
-
/* as above */
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (response?.status === 401) {
|
|
266
|
-
// The refresh (or the fresh-adopted token) was rejected:
|
|
267
|
-
// the session is dead. Retrying forever would spin pi's
|
|
268
|
-
// auto-retry loop silently — fail fast with the fix.
|
|
269
|
-
warnStderr(
|
|
270
|
-
"Omnirush: this device's session is no longer valid — run `omnirush login` to sign in again.",
|
|
271
|
-
);
|
|
272
|
-
process.exit(2);
|
|
273
|
-
}
|
|
244
|
+
// Every attempt carries the CURRENT on-disk token (re-read per
|
|
245
|
+
// request: another terminal or a sub-agent may have rotated the
|
|
246
|
+
// pair since this process started). A 401 adopts a newer on-disk
|
|
247
|
+
// pair or refreshes under the cross-process lock, then resends.
|
|
248
|
+
const recordUsage = (res: any) => {
|
|
249
|
+
try {
|
|
250
|
+
recordGatewayUsage(res?.headers);
|
|
251
|
+
} catch {
|
|
252
|
+
/* usage capture must never break the request */
|
|
274
253
|
}
|
|
254
|
+
};
|
|
255
|
+
const { response: answered, outcome: authOutcome } = await sendWithDeviceAuth(
|
|
256
|
+
(token: string) => doFetch(token || undefined),
|
|
257
|
+
sharedRefresher(),
|
|
258
|
+
{
|
|
259
|
+
fallbackToken: bearerFromInit(init) || bearerFromInit(input),
|
|
260
|
+
onResponse: (res: any) => {
|
|
261
|
+
debug(`provider fetch ${requestUrl} -> ${res?.status} (attempt ${attempt})`);
|
|
262
|
+
recordUsage(res);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
let response = answered;
|
|
267
|
+
if (authOutcome?.status === "dead") {
|
|
268
|
+
// The manager refused the refresh token that is on disk right
|
|
269
|
+
// now (re-read under the refresh lock): the device session is
|
|
270
|
+
// gone (revoked, expired, signed out). Retrying would spin pi's
|
|
271
|
+
// auto-retry loop silently — fail fast with the fix.
|
|
272
|
+
debug(`device session ended: ${authOutcome.reason} ${authOutcome.detail ?? ""}`);
|
|
273
|
+
warnStderr(sessionEndedMessage(authOutcome));
|
|
274
|
+
process.exit(2);
|
|
275
|
+
}
|
|
276
|
+
if (authOutcome?.status === "transient") {
|
|
277
|
+
// Could not reach the manager to refresh: not a sign-out. Retry
|
|
278
|
+
// like any other brief outage.
|
|
279
|
+
debug(`refresh unavailable: ${authOutcome.error}`);
|
|
280
|
+
response = new Response(
|
|
281
|
+
JSON.stringify({ error: { message: "Omnirush sign-in service unavailable", type: "omnirush_error", code: "auth_refresh_unavailable" } }),
|
|
282
|
+
{ status: 503, headers: { "content-type": "application/json" } },
|
|
283
|
+
);
|
|
275
284
|
}
|
|
276
285
|
const retryAfterSec = Number(response?.headers?.get?.("retry-after")) || undefined;
|
|
277
286
|
return { response, ref, retryAfterSec };
|
|
@@ -355,6 +364,8 @@ function tapFetch(
|
|
|
355
364
|
const onData = (data: string) => {
|
|
356
365
|
if (reported) return;
|
|
357
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;
|
|
358
369
|
if (served === undefined || !isSotaViolation(expectedModelId, served)) return;
|
|
359
370
|
reported = true;
|
|
360
371
|
onViolation(expectedModelId, served);
|
|
@@ -389,22 +400,60 @@ export default function (pi: any) {
|
|
|
389
400
|
const responses = openAIResponsesApi();
|
|
390
401
|
const strictSota = (): boolean => pi.getFlag("strict-sota") === true;
|
|
391
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
|
+
|
|
392
408
|
const streamWith =
|
|
393
409
|
(simple: boolean) =>
|
|
394
410
|
(model: any, context: any, options: any) => {
|
|
395
|
-
|
|
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
|
+
}
|
|
396
429
|
const tapped = {
|
|
397
430
|
...(options ?? {}),
|
|
398
431
|
fetch: tapFetch(
|
|
399
432
|
baseFetch,
|
|
400
|
-
|
|
433
|
+
() => route.expected,
|
|
401
434
|
(expected, served) => reportViolation(strictSota(), expected, served),
|
|
402
435
|
streamIdleMs(process.env.OMNIRUSH_STREAM_IDLE_MS, options?.timeoutMs),
|
|
403
436
|
),
|
|
404
437
|
};
|
|
405
|
-
|
|
438
|
+
const stream = simple
|
|
406
439
|
? responses.streamSimple(model, context, tapped)
|
|
407
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;
|
|
408
457
|
};
|
|
409
458
|
|
|
410
459
|
pi.registerProvider({
|
|
@@ -415,7 +464,15 @@ export default function (pi: any) {
|
|
|
415
464
|
apiKey: {
|
|
416
465
|
name: "Omnirush token",
|
|
417
466
|
async resolve() {
|
|
418
|
-
|
|
467
|
+
// The shared auth file first (always current); the launcher's
|
|
468
|
+
// environment value only as a bootstrap.
|
|
469
|
+
let key = "";
|
|
470
|
+
try {
|
|
471
|
+
key = sharedRefresher().accessToken();
|
|
472
|
+
} catch {
|
|
473
|
+
/* fall back to the environment */
|
|
474
|
+
}
|
|
475
|
+
key ||= (process.env.OMNIRUSH_TOKEN || "").trim();
|
|
419
476
|
if (!key) return undefined;
|
|
420
477
|
return { auth: { apiKey: key }, source: "environment" };
|
|
421
478
|
},
|
|
@@ -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;
|