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.
- package/assets/CHANGELOG.md +85 -0
- package/assets/extensions/omnirush/agents-lib.ts +134 -13
- package/assets/extensions/omnirush/agents.ts +51 -107
- package/assets/extensions/omnirush/bgshell-lib.ts +400 -0
- package/assets/extensions/omnirush/bgshell.ts +392 -0
- package/assets/extensions/omnirush/collector.ts +72 -11
- package/assets/extensions/omnirush/commands.ts +2 -0
- 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 +52 -4
- 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 +7 -3
- 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 +205 -33
- package/src/compat.js +272 -0
- package/src/lib.js +64 -0
- package/scripts/patch-pi-branding.js +0 -251
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Omnirush CLI release notes
|
|
2
|
+
|
|
3
|
+
Shown by `/changelog` inside the agent and after an update. Newest first;
|
|
4
|
+
keep entries free of links (the agent core rewrites relative links).
|
|
5
|
+
|
|
6
|
+
## [0.9.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Voice mode: `/voice` turns on dictation. Hold Space to talk and your words
|
|
10
|
+
are typed into the prompt at the cursor as you speak, ready to edit before
|
|
11
|
+
you send. Esc cancels and puts your draft back. `/voice tap` toggles with a
|
|
12
|
+
tap instead, `/voice key <key>` picks another key, `/voice auto-send on`
|
|
13
|
+
sends dictations of three words or more when you let go, and `/voice off`
|
|
14
|
+
turns it off. Audio is never saved to disk and never becomes part of your
|
|
15
|
+
session; only the text you send does.
|
|
16
|
+
- Long shell commands no longer get killed. A command still running after
|
|
17
|
+
five minutes (or the time the agent asked for) keeps going in the
|
|
18
|
+
background and its output arrives when it finishes; servers, watchers and
|
|
19
|
+
long builds can start in the background right away. `/jobs` lists them and
|
|
20
|
+
`/jobs kill <id|all>` stops them. `OMNIRUSH_BASH_FOREGROUND_SECONDS`
|
|
21
|
+
changes the five minutes.
|
|
22
|
+
- `/subagents` picks the model and effort sub-agents run on, as in the
|
|
23
|
+
desktop app: `/subagents model <id|same>`, `/subagents effort <level|same>`
|
|
24
|
+
and `/subagents reset`. The choice is remembered for the next runs;
|
|
25
|
+
`--subagent-model` / `--subagent-effort` (or `OMNIRUSH_SUBAGENT_MODEL` /
|
|
26
|
+
`OMNIRUSH_SUBAGENT_EFFORT`) set it for one run. If your account cannot use
|
|
27
|
+
the picked model, sub-agents run on your main model and say so. A task's
|
|
28
|
+
own `model:effort` still wins over the pick.
|
|
29
|
+
- Swarms: when you ask for a big job split across three or more sub-agents
|
|
30
|
+
(or ask for a swarm), omnirush coordinates them on a shared board in
|
|
31
|
+
`.omnirush/swarm.md` and archives it to `.omnirush/swarms/` when the swarm
|
|
32
|
+
ends. Smaller requests never create a board, and the board is kept out of
|
|
33
|
+
git.
|
|
34
|
+
- Guarded mode, on by default, as in the desktop app: before the agent
|
|
35
|
+
commits, pushes, rewrites git history, runs a destructive command
|
|
36
|
+
(`rm -rf`, `git reset --hard`, `git push --force` ...), uses `sudo`, reads
|
|
37
|
+
a `.env` file or touches files outside the project, omnirush shows you the
|
|
38
|
+
command and asks: allow once, allow for this session, or deny. Sub-agents
|
|
39
|
+
ask in the same place. Runs with `-p` cannot ask, so those calls are
|
|
40
|
+
blocked there with a note to rerun with `--yolo`.
|
|
41
|
+
- Yolo mode: `omnirush --yolo`, `/yolo on` or `/mode yolo` (remembered), or
|
|
42
|
+
`OMNIRUSH_YOLO=1`. Nothing asks, every command is allowed and the project
|
|
43
|
+
folder is trusted. The footer, `/status` and `/mode` show which mode you
|
|
44
|
+
are in.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- Omnirush everywhere: every screen, help page, message, file name and
|
|
48
|
+
update note now says omnirush.
|
|
49
|
+
- Your agent data (sessions, settings, models.json, provider logins,
|
|
50
|
+
installed packages) now lives in `~/.omnirush/agent`. It is copied over
|
|
51
|
+
automatically the first time 0.9.0 starts; the previous folder is left
|
|
52
|
+
untouched, and `--continue` and `/resume` find your earlier sessions.
|
|
53
|
+
- Project settings live in `<project>/.omnirush/`. A project that still has
|
|
54
|
+
its settings in the folder older versions used keeps working unchanged.
|
|
55
|
+
- Environment variables: every agent setting can be set as `OMNIRUSH_<NAME>`
|
|
56
|
+
(for example `OMNIRUSH_CACHE_RETENTION`); the older names still work.
|
|
57
|
+
`OMNIRUSH_AGENT_DIR` moves the agent data folder.
|
|
58
|
+
|
|
59
|
+
### Removed
|
|
60
|
+
- `/bug` and `/share`: omnirush no longer sends reports, sessions, install
|
|
61
|
+
reports, analytics or version checks to the agent core's upstream service.
|
|
62
|
+
|
|
63
|
+
## [0.8.6]
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- Sub-agents run on your session's current model and effort, and a task can
|
|
67
|
+
name its own as `model:effort` (for example `gpt-6-sol:high`).
|
|
68
|
+
- A footer line shows your sub-agents while they run: how many are running,
|
|
69
|
+
queued and finished.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- No more false "this device's session is no longer valid" after another
|
|
73
|
+
omnirush process refreshed the sign-in.
|
|
74
|
+
|
|
75
|
+
## [0.8.1]
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
- GPT 6 Sol, and the model list now comes from the gateway's catalog, as in
|
|
79
|
+
the desktop app.
|
|
80
|
+
|
|
81
|
+
## [0.8.0]
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
- Session capture in the desktop app's format, with sub-agents and project
|
|
85
|
+
archives.
|
|
@@ -28,7 +28,16 @@ import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
|
|
31
|
+
import {
|
|
32
|
+
ENV_FALLBACK_EFFORT,
|
|
33
|
+
ENV_FALLBACK_MODEL,
|
|
34
|
+
FALLBACK_MARKER,
|
|
35
|
+
fallbackNote,
|
|
36
|
+
piThinking,
|
|
37
|
+
type SubagentFallback,
|
|
38
|
+
} from "./subagents-lib";
|
|
31
39
|
import { childAuthEnv } from "./auth";
|
|
40
|
+
import { yoloActive } from "./yolo-lib";
|
|
32
41
|
|
|
33
42
|
/** childAuthEnv, never throwing (a spawn must not fail on the auth file). */
|
|
34
43
|
function childAuthEnvSafe(): Record<string, string> {
|
|
@@ -164,8 +173,15 @@ export function buildChildArgs(
|
|
|
164
173
|
promptFilePath: string | null,
|
|
165
174
|
model?: string,
|
|
166
175
|
sessionId?: string,
|
|
176
|
+
effort?: string | null,
|
|
177
|
+
approve = false,
|
|
167
178
|
): string[] {
|
|
168
179
|
const args: string[] = ["--mode", "json", "-p"];
|
|
180
|
+
if (approve) {
|
|
181
|
+
// Yolo mode: a headless child has no trust prompt, so without this it
|
|
182
|
+
// would ignore the project's settings the parent runs with.
|
|
183
|
+
args.push("--approve");
|
|
184
|
+
}
|
|
169
185
|
if (sessionId) {
|
|
170
186
|
// The child's session id, picked by the parent so its session file can
|
|
171
187
|
// be found and captured as this session's sub-agent.
|
|
@@ -177,6 +193,11 @@ export function buildChildArgs(
|
|
|
177
193
|
if (model && model.trim()) {
|
|
178
194
|
args.push("--provider", "omnirush", "--model", model.trim());
|
|
179
195
|
}
|
|
196
|
+
if (effort && effort.trim()) {
|
|
197
|
+
// The effort the sub-agent runs on (the picked one, or the main agent's
|
|
198
|
+
// mapped to the levels this model offers).
|
|
199
|
+
args.push("--thinking", piThinking(effort.trim()));
|
|
200
|
+
}
|
|
180
201
|
args.push(`Task: ${task}`);
|
|
181
202
|
return args;
|
|
182
203
|
}
|
|
@@ -207,6 +228,25 @@ export interface ChildTask {
|
|
|
207
228
|
* (e.g. "muse-spark-1.3" for cheap swarm workers under an astra
|
|
208
229
|
* parent). Undefined = inherit the parent's model. */
|
|
209
230
|
model?: string;
|
|
231
|
+
/** The effort the child runs on (gateway spelling); undefined = pi's default. */
|
|
232
|
+
effort?: string;
|
|
233
|
+
/** The picked sub-agent model could not be used: `model` is the main one. */
|
|
234
|
+
fallback?: SubagentFallback;
|
|
235
|
+
/** The main model the child's gateway guard moves to when the gateway refuses `model`. */
|
|
236
|
+
gatewayFallback?: { model: string; effort: string | null };
|
|
237
|
+
/** Extra environment for the child (the sub-agent setting and main model, for nested layers). */
|
|
238
|
+
env?: Record<string, string>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A sub-agent that ran on the main model instead of the picked one. */
|
|
242
|
+
export interface ChildModelFallback {
|
|
243
|
+
requested: string;
|
|
244
|
+
used: string;
|
|
245
|
+
effort?: string | null;
|
|
246
|
+
reason: string;
|
|
247
|
+
/** "selection": picked before it started; "gateway": the gateway refused the picked model mid-run. */
|
|
248
|
+
kind: "selection" | "gateway";
|
|
249
|
+
note: string;
|
|
210
250
|
}
|
|
211
251
|
|
|
212
252
|
export type ChildStatus = "completed" | "failed" | "timeout" | "stalled" | "cancelled" | "interrupted";
|
|
@@ -218,6 +258,10 @@ export interface ChildResult {
|
|
|
218
258
|
session_id?: string;
|
|
219
259
|
/** The gateway model the child ran on (cross-model children). */
|
|
220
260
|
model?: string;
|
|
261
|
+
/** The effort it ran on. */
|
|
262
|
+
effort?: string;
|
|
263
|
+
/** It ran on the main model instead of the picked one (why, and since when). */
|
|
264
|
+
model_fallback?: ChildModelFallback;
|
|
221
265
|
status: ChildStatus;
|
|
222
266
|
/** Process exit code (null when killed by a signal or still unknown). */
|
|
223
267
|
exitCode: number | null;
|
|
@@ -237,6 +281,33 @@ export interface ChildResultInput {
|
|
|
237
281
|
turns: number;
|
|
238
282
|
status?: ChildStatus;
|
|
239
283
|
error?: string;
|
|
284
|
+
/** The gateway moved the child to the main model mid-run. */
|
|
285
|
+
gatewayFallback?: { requested: string; used: string; effort?: string | null; reason: string };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The selection or gateway fallback of a child, for its result. */
|
|
289
|
+
export function childModelFallback(task: ChildTask, gateway?: ChildResultInput["gatewayFallback"]): ChildModelFallback | undefined {
|
|
290
|
+
if (task.fallback) {
|
|
291
|
+
return {
|
|
292
|
+
requested: task.fallback.requested,
|
|
293
|
+
used: task.fallback.used,
|
|
294
|
+
effort: task.effort ?? null,
|
|
295
|
+
reason: String(task.fallback.reason),
|
|
296
|
+
kind: "selection",
|
|
297
|
+
note: fallbackNote(task.fallback),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (gateway) {
|
|
301
|
+
return {
|
|
302
|
+
requested: gateway.requested,
|
|
303
|
+
used: gateway.used,
|
|
304
|
+
effort: gateway.effort ?? null,
|
|
305
|
+
reason: gateway.reason,
|
|
306
|
+
kind: "gateway",
|
|
307
|
+
note: fallbackNote({ requested: gateway.requested, used: gateway.used, reason: gateway.reason }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
240
311
|
}
|
|
241
312
|
|
|
242
313
|
/** Final structured result for one child. */
|
|
@@ -253,11 +324,14 @@ export function buildChildResult(
|
|
|
253
324
|
capped = output.slice(0, CHILD_OUTPUT_CAP_BYTES);
|
|
254
325
|
while (Buffer.byteLength(capped, "utf8") > CHILD_OUTPUT_CAP_BYTES) capped = capped.slice(0, -1);
|
|
255
326
|
}
|
|
327
|
+
const modelFallback = childModelFallback(task, input.gatewayFallback);
|
|
256
328
|
return {
|
|
257
329
|
role: task.role,
|
|
258
330
|
task: task.task,
|
|
259
331
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
260
332
|
...(task.model ? { model: task.model } : {}),
|
|
333
|
+
...(task.effort ? { effort: task.effort } : {}),
|
|
334
|
+
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
261
335
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
262
336
|
exitCode: input.exitCode,
|
|
263
337
|
output: capped,
|
|
@@ -392,13 +466,15 @@ export async function runChildAgent(
|
|
|
392
466
|
const sessionId = options.sessionId ?? randomUUID();
|
|
393
467
|
|
|
394
468
|
return withRolePromptFile(task.role, async (promptFile) => {
|
|
395
|
-
const args = buildChildArgs(task.task, promptFile, task.model, sessionId);
|
|
469
|
+
const args = buildChildArgs(task.task, promptFile, task.model, sessionId, task.effort, yoloActive());
|
|
396
470
|
const invocation = childInvocation(args);
|
|
397
471
|
|
|
398
472
|
return await new Promise<ChildResult>((resolvePromise) => {
|
|
399
473
|
let stdout = "";
|
|
400
474
|
let stderr = "";
|
|
401
475
|
let pendingLine = "";
|
|
476
|
+
let pendingErrLine = "";
|
|
477
|
+
let gatewayFallback: ChildResultInput["gatewayFallback"];
|
|
402
478
|
let settled = false;
|
|
403
479
|
let killedFor: "timeout" | "stalled" | "cancelled" | "interrupted" | null = null;
|
|
404
480
|
let turns = 0;
|
|
@@ -411,14 +487,10 @@ export async function runChildAgent(
|
|
|
411
487
|
cwd: options.cwd,
|
|
412
488
|
shell: false,
|
|
413
489
|
stdio: ["ignore", "pipe", "pipe"],
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// for every request and takes part in the refresh lock.
|
|
419
|
-
...childAuthEnvSafe(),
|
|
420
|
-
OMNIRUSH_PARENT_SESSION: options.parentSessionId,
|
|
421
|
-
},
|
|
490
|
+
// Shared credentials by location (OMNIRUSH_DIR), never a token
|
|
491
|
+
// frozen at the parent's launch: the child re-reads auth.json for
|
|
492
|
+
// every request and takes part in the refresh lock.
|
|
493
|
+
env: childEnvironment(task, options.parentSessionId, { ...process.env, ...childAuthEnvSafe() }),
|
|
422
494
|
});
|
|
423
495
|
|
|
424
496
|
const finish = (input: ChildResultInput) => {
|
|
@@ -429,7 +501,7 @@ export async function runChildAgent(
|
|
|
429
501
|
if (options.signal && abortHandler) {
|
|
430
502
|
options.signal.removeEventListener("abort", abortHandler);
|
|
431
503
|
}
|
|
432
|
-
resolvePromise(buildChildResult(task, input, now() - startedAt, sessionId));
|
|
504
|
+
resolvePromise(buildChildResult(task, { ...input, ...(gatewayFallback ? { gatewayFallback } : {}) }, now() - startedAt, sessionId));
|
|
433
505
|
};
|
|
434
506
|
|
|
435
507
|
const killTree = () => {
|
|
@@ -506,7 +578,12 @@ export async function runChildAgent(
|
|
|
506
578
|
options.onChildStdout?.(task.role, text);
|
|
507
579
|
});
|
|
508
580
|
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
509
|
-
|
|
581
|
+
const text = String(chunk);
|
|
582
|
+
// The child's gateway guard reports a move to the main model here.
|
|
583
|
+
const lines = (pendingErrLine + text).split("\n");
|
|
584
|
+
pendingErrLine = lines.pop() ?? "";
|
|
585
|
+
for (const line of lines) gatewayFallback = parseFallbackMarker(line) ?? gatewayFallback;
|
|
586
|
+
stderr += text;
|
|
510
587
|
if (stderr.length > 256 * 1024) stderr = stderr.slice(-64 * 1024);
|
|
511
588
|
alive();
|
|
512
589
|
});
|
|
@@ -551,6 +628,44 @@ function formatMinutes(ms: number): string {
|
|
|
551
628
|
return `${Math.max(1, Math.round(ms / 1000))} s`;
|
|
552
629
|
}
|
|
553
630
|
|
|
631
|
+
/**
|
|
632
|
+
* A child's environment: the parent's, the parent session id (the child
|
|
633
|
+
* uploads nothing itself), the sub-agent setting and main model handed down
|
|
634
|
+
* to nested layers, and the main model its gateway guard falls back to (only
|
|
635
|
+
* for this child: a nested one gets its own or none).
|
|
636
|
+
*/
|
|
637
|
+
export function childEnvironment(task: ChildTask, parentSessionId: string, base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
638
|
+
const env: NodeJS.ProcessEnv = { ...base, ...(task.env ?? {}), OMNIRUSH_PARENT_SESSION: parentSessionId };
|
|
639
|
+
// Guarded mode: the name the parent's approval prompt shows for this child.
|
|
640
|
+
const oneLine = task.task.replace(/\s+/g, " ").trim();
|
|
641
|
+
env.OMNIRUSH_SUBAGENT_LABEL = `${ROLE_PRESETS[task.role]?.label ?? task.role}: ${oneLine.length > 60 ? `${oneLine.slice(0, 59)}…` : oneLine}`;
|
|
642
|
+
delete env[ENV_FALLBACK_MODEL];
|
|
643
|
+
delete env[ENV_FALLBACK_EFFORT];
|
|
644
|
+
if (task.gatewayFallback?.model) {
|
|
645
|
+
env[ENV_FALLBACK_MODEL] = task.gatewayFallback.model;
|
|
646
|
+
if (task.gatewayFallback.effort) env[ENV_FALLBACK_EFFORT] = task.gatewayFallback.effort;
|
|
647
|
+
}
|
|
648
|
+
return env;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** A child's stderr line reporting its gateway fallback (see sota.ts), or null. */
|
|
652
|
+
export function parseFallbackMarker(line: string): ChildResultInput["gatewayFallback"] | null {
|
|
653
|
+
const at = line.indexOf(FALLBACK_MARKER);
|
|
654
|
+
if (at < 0) return null;
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(line.slice(at + FALLBACK_MARKER.length));
|
|
657
|
+
if (typeof parsed?.requested !== "string" || typeof parsed?.used !== "string") return null;
|
|
658
|
+
return {
|
|
659
|
+
requested: parsed.requested,
|
|
660
|
+
used: parsed.used,
|
|
661
|
+
effort: typeof parsed.effort === "string" ? parsed.effort : null,
|
|
662
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : "refused",
|
|
663
|
+
};
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
554
669
|
/**
|
|
555
670
|
* Run tasks with a concurrency cap, preserving input order in the
|
|
556
671
|
* results (ports the pi subagent example's mapWithConcurrencyLimit).
|
|
@@ -580,8 +695,12 @@ export function renderChildResults(results: ChildResult[], ids?: readonly string
|
|
|
580
695
|
const succeeded = results.filter((result) => result.status === "completed").length;
|
|
581
696
|
const sections = results.map((result, index) => {
|
|
582
697
|
const minutes = Math.round((result.durationMs / 60_000) * 10) / 10;
|
|
583
|
-
const
|
|
698
|
+
const ranOn = result.model_fallback?.kind === "gateway" ? result.model_fallback.used : result.model;
|
|
699
|
+
const effort = result.model_fallback?.kind === "gateway" ? result.model_fallback.effort ?? undefined : result.effort;
|
|
700
|
+
const label = ranOn ? ` [${ranOn}${effort ? ` · ${effort}` : ""}]` : "";
|
|
701
|
+
const header = `### ${ids?.[index] ? `${ids[index]} ` : ""}${result.role}${label} — ${result.status} (${minutes} min${result.outputTruncated ? ", output capped" : ""})`;
|
|
584
702
|
const meta: string[] = [`task: ${result.task}`];
|
|
703
|
+
if (result.model_fallback) meta.push(`note: ${result.model_fallback.note}`);
|
|
585
704
|
if (result.error) meta.push(`error: ${result.error}`);
|
|
586
705
|
return `${header}\n${meta.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
587
706
|
});
|
|
@@ -604,6 +723,7 @@ export interface AgentRecord {
|
|
|
604
723
|
role: AgentRole;
|
|
605
724
|
task: string;
|
|
606
725
|
model?: string;
|
|
726
|
+
effort?: string;
|
|
607
727
|
/** Dispatched with wait:false: its result comes back as a message. */
|
|
608
728
|
background: boolean;
|
|
609
729
|
/** Null means the batch was intentionally uncapped; otherwise the batch limit. */
|
|
@@ -699,6 +819,7 @@ export class AgentManager {
|
|
|
699
819
|
role: task.role,
|
|
700
820
|
task: task.task,
|
|
701
821
|
...(task.model ? { model: task.model } : {}),
|
|
822
|
+
...(task.effort ? { effort: task.effort } : {}),
|
|
702
823
|
background: options.background,
|
|
703
824
|
parallelLimit: options.concurrency === undefined ? null : Math.max(1, Math.floor(options.concurrency)),
|
|
704
825
|
notify: options.notify ?? "batch",
|
|
@@ -944,7 +1065,7 @@ export function renderAgentStatus(records: AgentRecord[], now: number = Date.now
|
|
|
944
1065
|
const since = record.startedAt ?? record.queuedAt;
|
|
945
1066
|
const elapsed = minutesOf((record.finishedAt ?? now) - since);
|
|
946
1067
|
const bits = [`${record.id}`, `[${record.status}]`, record.role];
|
|
947
|
-
if (record.model) bits.push(`[${record.model}]`);
|
|
1068
|
+
if (record.model) bits.push(`[${record.model}${record.effort ? ` · ${record.effort}` : ""}]`);
|
|
948
1069
|
bits.push(record.background ? "background" : "blocking");
|
|
949
1070
|
const detail: string[] = [`${elapsed} min`];
|
|
950
1071
|
if (!record.result && record.startedAt) {
|
|
@@ -36,6 +36,8 @@ 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,
|
|
@@ -48,11 +50,12 @@ import {
|
|
|
48
50
|
renderAgentStatus,
|
|
49
51
|
renderChildResults,
|
|
50
52
|
renderDelivery,
|
|
51
|
-
modelSelectionFromContext,
|
|
52
53
|
runChildAgent,
|
|
54
|
+
childModelFallback,
|
|
53
55
|
type ChildResult,
|
|
54
56
|
type ChildTask,
|
|
55
57
|
} from "./agents-lib";
|
|
58
|
+
import { SWARM_GUIDELINE } from "./swarm-lib";
|
|
56
59
|
|
|
57
60
|
/** A timeout_minutes below this is taken for a placeholder, not a limit. */
|
|
58
61
|
const MIN_TIMEOUT_MINUTES = 1;
|
|
@@ -68,7 +71,7 @@ const SpawnAgentsParams = Type.Object({
|
|
|
68
71
|
}),
|
|
69
72
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
70
73
|
model: Type.Optional(Type.String({
|
|
71
|
-
description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "gpt-6-sol:high"). Omit (or leave empty)
|
|
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',
|
|
72
75
|
})),
|
|
73
76
|
}),
|
|
74
77
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
@@ -108,19 +111,14 @@ function renderSome(records: AgentRecord[], unknown: string[] = []): string {
|
|
|
108
111
|
|
|
109
112
|
export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {}) {
|
|
110
113
|
const watchdogMinutes = Math.round(childInactivityMs() / 60_000);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let cwd = process.cwd();
|
|
114
|
-
let inheritedModel = "";
|
|
114
|
+
const hub = deliveryHub(pi);
|
|
115
|
+
/** The last context seen (the TUI status bar for running agents). */
|
|
115
116
|
let lastContext: any = null;
|
|
116
117
|
let statusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
117
|
-
/** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
|
|
118
|
-
let busy = false;
|
|
119
|
-
let oneShot = false;
|
|
120
118
|
|
|
121
119
|
function publishStatus(): void {
|
|
122
120
|
statusTimer = null;
|
|
123
|
-
const session = currentSession;
|
|
121
|
+
const session = hub.currentSession;
|
|
124
122
|
if (!session || !lastContext?.ui?.setStatus) return;
|
|
125
123
|
const records = manager.list(session);
|
|
126
124
|
if (records.length > 0) lastContext.ui.setStatus("omnirush-agents", agentStatusSummary(records));
|
|
@@ -132,31 +130,9 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
132
130
|
statusTimer.unref?.();
|
|
133
131
|
}
|
|
134
132
|
|
|
135
|
-
const send = (records: AgentRecord[]) => {
|
|
136
|
-
pi.sendMessage(
|
|
137
|
-
{
|
|
138
|
-
customType: DELIVERY_TYPE,
|
|
139
|
-
content: renderDelivery(records),
|
|
140
|
-
display: true,
|
|
141
|
-
details: { results: records.map(resultOf) },
|
|
142
|
-
},
|
|
143
|
-
// Busy: into the running turn at its next step. Idle: a new turn.
|
|
144
|
-
{ triggerTurn: true, deliverAs: "steer" },
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const deliver = () => {
|
|
149
|
-
if (!currentSession) return;
|
|
150
|
-
// One-shot and between runs: the agent_settled loop delivers.
|
|
151
|
-
if (oneShot && !busy) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const group of manager.takeDeliverable(currentSession)) send(group.records);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
133
|
const manager = new AgentManager({
|
|
158
134
|
run: options.run ?? ((task, options) => runChildAgent(task, {
|
|
159
|
-
cwd: options.cwd ?? cwd,
|
|
135
|
+
cwd: options.cwd ?? hub.cwd,
|
|
160
136
|
parentSessionId: options.parentSessionId,
|
|
161
137
|
sessionId: options.sessionId,
|
|
162
138
|
signal: options.signal,
|
|
@@ -164,10 +140,21 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
164
140
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
165
141
|
})),
|
|
166
142
|
// After the settle's own bookkeeping (waiters took theirs).
|
|
167
|
-
onDeliverable: () =>
|
|
143
|
+
onDeliverable: () => hub.notify(),
|
|
168
144
|
onChanged: scheduleStatus,
|
|
169
145
|
});
|
|
170
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(),
|
|
156
|
+
});
|
|
157
|
+
|
|
171
158
|
const hook: AgentsHook = {
|
|
172
159
|
async interruptAll(parentSessionId?: string) {
|
|
173
160
|
const stopped = await manager.interruptAll(parentSessionId);
|
|
@@ -177,17 +164,8 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
177
164
|
(globalThis as any)[AGENTS_HOOK] = hook;
|
|
178
165
|
|
|
179
166
|
const sessionOf = (ctx: any): string => {
|
|
180
|
-
const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
181
|
-
if (id && id !== currentSession) {
|
|
182
|
-
currentSession = id;
|
|
183
|
-
inheritedModel = "";
|
|
184
|
-
}
|
|
185
167
|
lastContext = ctx ?? lastContext;
|
|
186
|
-
|
|
187
|
-
if (selected) inheritedModel = selected;
|
|
188
|
-
if (ctx?.cwd) cwd = String(ctx.cwd);
|
|
189
|
-
if (ctx?.mode === "print" || ctx?.mode === "json") oneShot = true;
|
|
190
|
-
return id;
|
|
168
|
+
return hub.sessionOf(ctx);
|
|
191
169
|
};
|
|
192
170
|
|
|
193
171
|
pi.registerTool({
|
|
@@ -212,6 +190,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
212
190
|
"Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
|
|
213
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.",
|
|
214
192
|
"Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
|
|
193
|
+
SWARM_GUIDELINE,
|
|
215
194
|
],
|
|
216
195
|
parameters: SpawnAgentsParams,
|
|
217
196
|
|
|
@@ -222,16 +201,22 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
222
201
|
if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
|
|
223
202
|
throw new Error("invalid tasks: every entry needs a non-empty task string");
|
|
224
203
|
}
|
|
225
|
-
//
|
|
226
|
-
//
|
|
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).
|
|
227
207
|
const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
228
208
|
tasks.push({
|
|
229
209
|
role: raw.role,
|
|
230
210
|
task: raw.task.trim(),
|
|
231
|
-
...(
|
|
211
|
+
...(requestedModel ? { model: requestedModel } : {}),
|
|
232
212
|
});
|
|
233
213
|
}
|
|
234
214
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
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);
|
|
235
220
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
236
221
|
const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
237
222
|
// Only a deliberate limit counts: models fill optional numbers with
|
|
@@ -247,7 +232,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
247
232
|
: undefined;
|
|
248
233
|
const background = params.wait === false;
|
|
249
234
|
|
|
250
|
-
const dispatched = manager.dispatch(parentSessionId,
|
|
235
|
+
const dispatched = manager.dispatch(parentSessionId, resolvedTasks, {
|
|
251
236
|
background,
|
|
252
237
|
concurrency,
|
|
253
238
|
notify: params.notify === "each" ? "each" : "batch",
|
|
@@ -271,7 +256,10 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
271
256
|
});
|
|
272
257
|
|
|
273
258
|
if (background) {
|
|
274
|
-
const lines = dispatched.records.map((record) =>
|
|
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
|
+
});
|
|
275
263
|
return {
|
|
276
264
|
content: [{
|
|
277
265
|
type: "text",
|
|
@@ -288,15 +276,20 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
288
276
|
details: {
|
|
289
277
|
background: true,
|
|
290
278
|
batch: dispatched.batch,
|
|
291
|
-
results: dispatched.records.map((record) =>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}),
|
|
300
293
|
},
|
|
301
294
|
};
|
|
302
295
|
}
|
|
@@ -404,55 +397,6 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
404
397
|
},
|
|
405
398
|
});
|
|
406
399
|
|
|
407
|
-
pi.on("session_start", (_event: any, ctx: any) => {
|
|
408
|
-
sessionOf(ctx);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
pi.on("agent_start", (_event: any, ctx: any) => {
|
|
412
|
-
sessionOf(ctx);
|
|
413
|
-
busy = true;
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* One-shot runs (print/json mode) end when the agent settles: background
|
|
418
|
-
* children still out would be lost. The run waits for them here and
|
|
419
|
-
* continues with their results (a new run nested in this settle, whose
|
|
420
|
-
* own settle resumes this one when everything is done).
|
|
421
|
-
*/
|
|
422
|
-
let resumeOuter: (() => void) | null = null;
|
|
423
|
-
pi.on("agent_settled", async (_event: any, ctx: any) => {
|
|
424
|
-
busy = false;
|
|
425
|
-
const session = sessionOf(ctx);
|
|
426
|
-
if (!oneShot) {
|
|
427
|
-
// Anything that finished right as the run ended.
|
|
428
|
-
deliver();
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
const outer = resumeOuter;
|
|
432
|
-
resumeOuter = null;
|
|
433
|
-
try {
|
|
434
|
-
for (;;) {
|
|
435
|
-
if (typeof ctx?.isIdle === "function" && !ctx.isIdle()) {
|
|
436
|
-
// A run started (a delivery): its settle resumes this one.
|
|
437
|
-
await new Promise<void>((resolve) => { resumeOuter = resolve; });
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
if (!session || !manager.hasPending(session)) break;
|
|
441
|
-
const groups = manager.takeDeliverable(session);
|
|
442
|
-
if (groups.length === 0) {
|
|
443
|
-
await manager.changed();
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
busy = true;
|
|
447
|
-
const records = groups.flatMap((group) => group.records);
|
|
448
|
-
send(records);
|
|
449
|
-
if (typeof ctx?.isIdle === "function" && ctx.isIdle()) busy = false;
|
|
450
|
-
}
|
|
451
|
-
} finally {
|
|
452
|
-
outer?.();
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
400
|
pi.on("session_shutdown", async () => {
|
|
457
401
|
// Children the session ends under are stopped (the collector, when it
|
|
458
402
|
// runs, already did this before its last capture).
|