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
|
@@ -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.
|
package/assets/collect-once.ts
CHANGED
|
@@ -43,7 +43,9 @@ async function main() {
|
|
|
43
43
|
const { dir, dryRun } = parseArgs(process.argv.slice(2));
|
|
44
44
|
const sessionId = `manual-${randomBytes(6).toString("hex")}`;
|
|
45
45
|
const workspaceId = `cli_${createHash("sha256").update(path.resolve(dir)).digest("hex").slice(0, 16)}`;
|
|
46
|
-
const accessToken = (
|
|
46
|
+
const accessToken = (
|
|
47
|
+
sharedRefresher().accessToken() || process.env.OMNIRUSH_ACCESS_TOKEN || process.env.OMNIRUSH_TOKEN || ""
|
|
48
|
+
).trim();
|
|
47
49
|
if (!accessToken && !dryRun) {
|
|
48
50
|
console.error("omnirush collect: no access token (run `omnirush login`)");
|
|
49
51
|
process.exit(1);
|
|
@@ -57,10 +59,9 @@ async function main() {
|
|
|
57
59
|
// A dry run keeps no state (nothing is spooled for a later run).
|
|
58
60
|
...(dryRun ? {} : { stateDir: omniDir() }),
|
|
59
61
|
refreshAccessToken: async () => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const next =
|
|
63
|
-
if (!refreshed || !next) return null;
|
|
62
|
+
const outcome = await sharedRefresher().recover(current).catch(() => null);
|
|
63
|
+
if (outcome?.status !== "ok") return null;
|
|
64
|
+
const next = outcome.accessToken;
|
|
64
65
|
current = next;
|
|
65
66
|
return next;
|
|
66
67
|
},
|
|
@@ -28,6 +28,26 @@ 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";
|
|
39
|
+
import { childAuthEnv } from "./auth";
|
|
40
|
+
import { yoloActive } from "./yolo-lib";
|
|
41
|
+
|
|
42
|
+
/** childAuthEnv, never throwing (a spawn must not fail on the auth file). */
|
|
43
|
+
function childAuthEnvSafe(): Record<string, string> {
|
|
44
|
+
try {
|
|
45
|
+
return childAuthEnv(process.env);
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Inactivity watchdog default: a child that prints nothing (no model
|
|
33
53
|
* stream, no tool event) for this long is treated as hung and stopped.
|
|
@@ -55,6 +75,24 @@ export const CHILD_OUTPUT_CAP_BYTES = 50 * 1024;
|
|
|
55
75
|
export const AGENT_ROLES = ["code-searcher", "researcher-web", "general-worker"] as const;
|
|
56
76
|
export type AgentRole = (typeof AGENT_ROLES)[number];
|
|
57
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the model selected for the current parent session. Pi keeps the
|
|
80
|
+
* effort level separately from the model id, while the CLI accepts the
|
|
81
|
+
* canonical `model:effort` spelling for child argv.
|
|
82
|
+
*/
|
|
83
|
+
export function modelSelectionFromContext(ctx: any): string | undefined {
|
|
84
|
+
const model = ctx?.model;
|
|
85
|
+
const id = typeof model?.id === "string"
|
|
86
|
+
? model.id.trim()
|
|
87
|
+
: typeof model?.modelID === "string"
|
|
88
|
+
? model.modelID.trim()
|
|
89
|
+
: "";
|
|
90
|
+
if (!id) return undefined;
|
|
91
|
+
const effort = typeof ctx?.thinkingLevel === "string" ? ctx.thinkingLevel.trim() : "";
|
|
92
|
+
if (!effort || effort === "off" || id.includes(":")) return id;
|
|
93
|
+
return `${id}:${effort}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
58
96
|
export interface RolePreset {
|
|
59
97
|
role: AgentRole;
|
|
60
98
|
label: string;
|
|
@@ -135,8 +173,15 @@ export function buildChildArgs(
|
|
|
135
173
|
promptFilePath: string | null,
|
|
136
174
|
model?: string,
|
|
137
175
|
sessionId?: string,
|
|
176
|
+
effort?: string | null,
|
|
177
|
+
approve = false,
|
|
138
178
|
): string[] {
|
|
139
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
|
+
}
|
|
140
185
|
if (sessionId) {
|
|
141
186
|
// The child's session id, picked by the parent so its session file can
|
|
142
187
|
// be found and captured as this session's sub-agent.
|
|
@@ -148,6 +193,11 @@ export function buildChildArgs(
|
|
|
148
193
|
if (model && model.trim()) {
|
|
149
194
|
args.push("--provider", "omnirush", "--model", model.trim());
|
|
150
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
|
+
}
|
|
151
201
|
args.push(`Task: ${task}`);
|
|
152
202
|
return args;
|
|
153
203
|
}
|
|
@@ -178,6 +228,25 @@ export interface ChildTask {
|
|
|
178
228
|
* (e.g. "muse-spark-1.3" for cheap swarm workers under an astra
|
|
179
229
|
* parent). Undefined = inherit the parent's model. */
|
|
180
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;
|
|
181
250
|
}
|
|
182
251
|
|
|
183
252
|
export type ChildStatus = "completed" | "failed" | "timeout" | "stalled" | "cancelled" | "interrupted";
|
|
@@ -189,6 +258,10 @@ export interface ChildResult {
|
|
|
189
258
|
session_id?: string;
|
|
190
259
|
/** The gateway model the child ran on (cross-model children). */
|
|
191
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;
|
|
192
265
|
status: ChildStatus;
|
|
193
266
|
/** Process exit code (null when killed by a signal or still unknown). */
|
|
194
267
|
exitCode: number | null;
|
|
@@ -208,6 +281,33 @@ export interface ChildResultInput {
|
|
|
208
281
|
turns: number;
|
|
209
282
|
status?: ChildStatus;
|
|
210
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;
|
|
211
311
|
}
|
|
212
312
|
|
|
213
313
|
/** Final structured result for one child. */
|
|
@@ -224,11 +324,14 @@ export function buildChildResult(
|
|
|
224
324
|
capped = output.slice(0, CHILD_OUTPUT_CAP_BYTES);
|
|
225
325
|
while (Buffer.byteLength(capped, "utf8") > CHILD_OUTPUT_CAP_BYTES) capped = capped.slice(0, -1);
|
|
226
326
|
}
|
|
327
|
+
const modelFallback = childModelFallback(task, input.gatewayFallback);
|
|
227
328
|
return {
|
|
228
329
|
role: task.role,
|
|
229
330
|
task: task.task,
|
|
230
331
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
231
332
|
...(task.model ? { model: task.model } : {}),
|
|
333
|
+
...(task.effort ? { effort: task.effort } : {}),
|
|
334
|
+
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
232
335
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
233
336
|
exitCode: input.exitCode,
|
|
234
337
|
output: capped,
|
|
@@ -363,13 +466,15 @@ export async function runChildAgent(
|
|
|
363
466
|
const sessionId = options.sessionId ?? randomUUID();
|
|
364
467
|
|
|
365
468
|
return withRolePromptFile(task.role, async (promptFile) => {
|
|
366
|
-
const args = buildChildArgs(task.task, promptFile, task.model, sessionId);
|
|
469
|
+
const args = buildChildArgs(task.task, promptFile, task.model, sessionId, task.effort, yoloActive());
|
|
367
470
|
const invocation = childInvocation(args);
|
|
368
471
|
|
|
369
472
|
return await new Promise<ChildResult>((resolvePromise) => {
|
|
370
473
|
let stdout = "";
|
|
371
474
|
let stderr = "";
|
|
372
475
|
let pendingLine = "";
|
|
476
|
+
let pendingErrLine = "";
|
|
477
|
+
let gatewayFallback: ChildResultInput["gatewayFallback"];
|
|
373
478
|
let settled = false;
|
|
374
479
|
let killedFor: "timeout" | "stalled" | "cancelled" | "interrupted" | null = null;
|
|
375
480
|
let turns = 0;
|
|
@@ -382,10 +487,10 @@ export async function runChildAgent(
|
|
|
382
487
|
cwd: options.cwd,
|
|
383
488
|
shell: false,
|
|
384
489
|
stdio: ["ignore", "pipe", "pipe"],
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
},
|
|
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() }),
|
|
389
494
|
});
|
|
390
495
|
|
|
391
496
|
const finish = (input: ChildResultInput) => {
|
|
@@ -396,7 +501,7 @@ export async function runChildAgent(
|
|
|
396
501
|
if (options.signal && abortHandler) {
|
|
397
502
|
options.signal.removeEventListener("abort", abortHandler);
|
|
398
503
|
}
|
|
399
|
-
resolvePromise(buildChildResult(task, input, now() - startedAt, sessionId));
|
|
504
|
+
resolvePromise(buildChildResult(task, { ...input, ...(gatewayFallback ? { gatewayFallback } : {}) }, now() - startedAt, sessionId));
|
|
400
505
|
};
|
|
401
506
|
|
|
402
507
|
const killTree = () => {
|
|
@@ -473,7 +578,12 @@ export async function runChildAgent(
|
|
|
473
578
|
options.onChildStdout?.(task.role, text);
|
|
474
579
|
});
|
|
475
580
|
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
476
|
-
|
|
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;
|
|
477
587
|
if (stderr.length > 256 * 1024) stderr = stderr.slice(-64 * 1024);
|
|
478
588
|
alive();
|
|
479
589
|
});
|
|
@@ -518,6 +628,44 @@ function formatMinutes(ms: number): string {
|
|
|
518
628
|
return `${Math.max(1, Math.round(ms / 1000))} s`;
|
|
519
629
|
}
|
|
520
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
|
+
|
|
521
669
|
/**
|
|
522
670
|
* Run tasks with a concurrency cap, preserving input order in the
|
|
523
671
|
* results (ports the pi subagent example's mapWithConcurrencyLimit).
|
|
@@ -547,8 +695,12 @@ export function renderChildResults(results: ChildResult[], ids?: readonly string
|
|
|
547
695
|
const succeeded = results.filter((result) => result.status === "completed").length;
|
|
548
696
|
const sections = results.map((result, index) => {
|
|
549
697
|
const minutes = Math.round((result.durationMs / 60_000) * 10) / 10;
|
|
550
|
-
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" : ""})`;
|
|
551
702
|
const meta: string[] = [`task: ${result.task}`];
|
|
703
|
+
if (result.model_fallback) meta.push(`note: ${result.model_fallback.note}`);
|
|
552
704
|
if (result.error) meta.push(`error: ${result.error}`);
|
|
553
705
|
return `${header}\n${meta.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
554
706
|
});
|
|
@@ -571,8 +723,11 @@ export interface AgentRecord {
|
|
|
571
723
|
role: AgentRole;
|
|
572
724
|
task: string;
|
|
573
725
|
model?: string;
|
|
726
|
+
effort?: string;
|
|
574
727
|
/** Dispatched with wait:false: its result comes back as a message. */
|
|
575
728
|
background: boolean;
|
|
729
|
+
/** Null means the batch was intentionally uncapped; otherwise the batch limit. */
|
|
730
|
+
parallelLimit: number | null;
|
|
576
731
|
/** Background delivery: one message per child, or one per batch. */
|
|
577
732
|
notify: "each" | "batch";
|
|
578
733
|
status: AgentState;
|
|
@@ -611,6 +766,8 @@ export interface DispatchOptions {
|
|
|
611
766
|
signal?: AbortSignal;
|
|
612
767
|
/** A child settled (progress updates). */
|
|
613
768
|
onSettled?: (record: AgentRecord) => void;
|
|
769
|
+
/** A child was queued, started, updated, or settled. */
|
|
770
|
+
onChanged?: () => void;
|
|
614
771
|
/** Explicit wall-clock limit per child (none by default). */
|
|
615
772
|
timeoutMs?: number;
|
|
616
773
|
/** Workspace the children run in. */
|
|
@@ -627,6 +784,7 @@ export class AgentManager {
|
|
|
627
784
|
private readonly runner: AgentRunner;
|
|
628
785
|
private readonly now: () => number;
|
|
629
786
|
private readonly onDeliverable: () => void;
|
|
787
|
+
private readonly onChanged: () => void;
|
|
630
788
|
private readonly bySession = new Map<string, AgentRecord[]>();
|
|
631
789
|
private waiters: Array<{ records: AgentRecord[]; resolve: () => void }> = [];
|
|
632
790
|
private changeWaiters: Array<() => void> = [];
|
|
@@ -634,10 +792,11 @@ export class AgentManager {
|
|
|
634
792
|
private nextAgent = 1;
|
|
635
793
|
private nextBatch = 1;
|
|
636
794
|
|
|
637
|
-
constructor(options: { run: AgentRunner; now?: () => number; onDeliverable?: () => void }) {
|
|
795
|
+
constructor(options: { run: AgentRunner; now?: () => number; onDeliverable?: () => void; onChanged?: () => void }) {
|
|
638
796
|
this.runner = options.run;
|
|
639
797
|
this.now = options.now ?? Date.now;
|
|
640
798
|
this.onDeliverable = options.onDeliverable ?? (() => undefined);
|
|
799
|
+
this.onChanged = options.onChanged ?? (() => undefined);
|
|
641
800
|
}
|
|
642
801
|
|
|
643
802
|
dispatch(
|
|
@@ -660,7 +819,9 @@ export class AgentManager {
|
|
|
660
819
|
role: task.role,
|
|
661
820
|
task: task.task,
|
|
662
821
|
...(task.model ? { model: task.model } : {}),
|
|
822
|
+
...(task.effort ? { effort: task.effort } : {}),
|
|
663
823
|
background: options.background,
|
|
824
|
+
parallelLimit: options.concurrency === undefined ? null : Math.max(1, Math.floor(options.concurrency)),
|
|
664
825
|
notify: options.notify ?? "batch",
|
|
665
826
|
status: "queued",
|
|
666
827
|
queuedAt: at,
|
|
@@ -679,6 +840,8 @@ export class AgentManager {
|
|
|
679
840
|
list.push(record);
|
|
680
841
|
return record;
|
|
681
842
|
});
|
|
843
|
+
this.onChanged();
|
|
844
|
+
options.onChanged?.();
|
|
682
845
|
if (options.signal) {
|
|
683
846
|
const abort = () => { void this.cancel(records); };
|
|
684
847
|
if (options.signal.aborted) abort();
|
|
@@ -697,6 +860,8 @@ export class AgentManager {
|
|
|
697
860
|
record.status = "running";
|
|
698
861
|
record.startedAt = this.now();
|
|
699
862
|
record.lastActivityAt = record.startedAt;
|
|
863
|
+
this.onChanged();
|
|
864
|
+
options.onChanged?.();
|
|
700
865
|
let result: ChildResult;
|
|
701
866
|
try {
|
|
702
867
|
result = await this.runner(task, {
|
|
@@ -709,6 +874,8 @@ export class AgentManager {
|
|
|
709
874
|
record.lastActivityAt = activity.at;
|
|
710
875
|
record.turns = activity.turns;
|
|
711
876
|
record.toolsRunning = activity.toolsRunning;
|
|
877
|
+
this.onChanged();
|
|
878
|
+
options.onChanged?.();
|
|
712
879
|
},
|
|
713
880
|
});
|
|
714
881
|
} catch (error) {
|
|
@@ -730,6 +897,9 @@ export class AgentManager {
|
|
|
730
897
|
record.finishedAt = this.now();
|
|
731
898
|
record.turns = result.turns;
|
|
732
899
|
record.toolsRunning = 0;
|
|
900
|
+
this.onChanged();
|
|
901
|
+
// The dispatch callback is not available here; settle callers also invoke
|
|
902
|
+
// their onSettled hook immediately after start() returns.
|
|
733
903
|
this.resolvers.get(record)?.(result);
|
|
734
904
|
this.resolvers.delete(record);
|
|
735
905
|
// Waiters first: what they return is delivered through their tool result.
|
|
@@ -873,6 +1043,21 @@ function minutesOf(ms: number): number {
|
|
|
873
1043
|
return Math.round((ms / 60_000) * 10) / 10;
|
|
874
1044
|
}
|
|
875
1045
|
|
|
1046
|
+
/** Compact status used by both the tool stream and the TUI status bar. */
|
|
1047
|
+
export function agentStatusSummary(records: AgentRecord[]): string {
|
|
1048
|
+
if (records.length === 0) return "No sub-agents in this session.";
|
|
1049
|
+
const running = records.filter((record) => record.status === "running").length;
|
|
1050
|
+
const queued = records.filter((record) => record.status === "queued").length;
|
|
1051
|
+
const settled = records.filter((record) => Boolean(record.result)).length;
|
|
1052
|
+
const limits = [...new Set(records.map((record) => record.parallelLimit))];
|
|
1053
|
+
const parallel = limits.length === 1 && limits[0] === null
|
|
1054
|
+
? "unlimited"
|
|
1055
|
+
: limits.length === 1
|
|
1056
|
+
? String(limits[0])
|
|
1057
|
+
: "per-batch";
|
|
1058
|
+
return `${records.length} sub-agents: ${running} running, ${queued} queued, ${settled} settled (parallel: ${parallel})`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
876
1061
|
/** One status line per sub-agent (agents_status, /agents). */
|
|
877
1062
|
export function renderAgentStatus(records: AgentRecord[], now: number = Date.now()): string {
|
|
878
1063
|
if (records.length === 0) return "No sub-agents in this session.";
|
|
@@ -880,7 +1065,7 @@ export function renderAgentStatus(records: AgentRecord[], now: number = Date.now
|
|
|
880
1065
|
const since = record.startedAt ?? record.queuedAt;
|
|
881
1066
|
const elapsed = minutesOf((record.finishedAt ?? now) - since);
|
|
882
1067
|
const bits = [`${record.id}`, `[${record.status}]`, record.role];
|
|
883
|
-
if (record.model) bits.push(`[${record.model}]`);
|
|
1068
|
+
if (record.model) bits.push(`[${record.model}${record.effort ? ` · ${record.effort}` : ""}]`);
|
|
884
1069
|
bits.push(record.background ? "background" : "blocking");
|
|
885
1070
|
const detail: string[] = [`${elapsed} min`];
|
|
886
1071
|
if (!record.result && record.startedAt) {
|
|
@@ -892,8 +1077,7 @@ export function renderAgentStatus(records: AgentRecord[], now: number = Date.now
|
|
|
892
1077
|
const task = record.task.length > 80 ? `${record.task.slice(0, 79)}…` : record.task;
|
|
893
1078
|
return `- ${bits.join(" ")} (${detail.join(", ")}) — ${task}`;
|
|
894
1079
|
});
|
|
895
|
-
|
|
896
|
-
return `${records.length} sub-agents, ${running} running:\n${lines.join("\n")}`;
|
|
1080
|
+
return `${agentStatusSummary(records)}:\n${lines.join("\n")}`;
|
|
897
1081
|
}
|
|
898
1082
|
|
|
899
1083
|
/** The message that brings finished background agents back to the parent. */
|