omnirush 0.8.5 → 0.8.6
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/collect-once.ts +6 -5
- package/assets/extensions/omnirush/agents-lib.ts +66 -3
- package/assets/extensions/omnirush/agents.ts +62 -14
- package/assets/extensions/omnirush/auth.js +399 -115
- package/assets/extensions/omnirush/collector.ts +31 -5
- package/assets/extensions/omnirush/commands.ts +12 -15
- package/assets/extensions/omnirush/sota.ts +52 -43
- package/package.json +2 -2
- package/src/bin.js +48 -21
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,17 @@ 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 { childAuthEnv } from "./auth";
|
|
32
|
+
|
|
33
|
+
/** childAuthEnv, never throwing (a spawn must not fail on the auth file). */
|
|
34
|
+
function childAuthEnvSafe(): Record<string, string> {
|
|
35
|
+
try {
|
|
36
|
+
return childAuthEnv(process.env);
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Inactivity watchdog default: a child that prints nothing (no model
|
|
33
44
|
* stream, no tool event) for this long is treated as hung and stopped.
|
|
@@ -55,6 +66,24 @@ export const CHILD_OUTPUT_CAP_BYTES = 50 * 1024;
|
|
|
55
66
|
export const AGENT_ROLES = ["code-searcher", "researcher-web", "general-worker"] as const;
|
|
56
67
|
export type AgentRole = (typeof AGENT_ROLES)[number];
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the model selected for the current parent session. Pi keeps the
|
|
71
|
+
* effort level separately from the model id, while the CLI accepts the
|
|
72
|
+
* canonical `model:effort` spelling for child argv.
|
|
73
|
+
*/
|
|
74
|
+
export function modelSelectionFromContext(ctx: any): string | undefined {
|
|
75
|
+
const model = ctx?.model;
|
|
76
|
+
const id = typeof model?.id === "string"
|
|
77
|
+
? model.id.trim()
|
|
78
|
+
: typeof model?.modelID === "string"
|
|
79
|
+
? model.modelID.trim()
|
|
80
|
+
: "";
|
|
81
|
+
if (!id) return undefined;
|
|
82
|
+
const effort = typeof ctx?.thinkingLevel === "string" ? ctx.thinkingLevel.trim() : "";
|
|
83
|
+
if (!effort || effort === "off" || id.includes(":")) return id;
|
|
84
|
+
return `${id}:${effort}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
export interface RolePreset {
|
|
59
88
|
role: AgentRole;
|
|
60
89
|
label: string;
|
|
@@ -384,6 +413,10 @@ export async function runChildAgent(
|
|
|
384
413
|
stdio: ["ignore", "pipe", "pipe"],
|
|
385
414
|
env: {
|
|
386
415
|
...process.env,
|
|
416
|
+
// Shared credentials by location (OMNIRUSH_DIR), never a token
|
|
417
|
+
// frozen at the parent's launch: the child re-reads auth.json
|
|
418
|
+
// for every request and takes part in the refresh lock.
|
|
419
|
+
...childAuthEnvSafe(),
|
|
387
420
|
OMNIRUSH_PARENT_SESSION: options.parentSessionId,
|
|
388
421
|
},
|
|
389
422
|
});
|
|
@@ -573,6 +606,8 @@ export interface AgentRecord {
|
|
|
573
606
|
model?: string;
|
|
574
607
|
/** Dispatched with wait:false: its result comes back as a message. */
|
|
575
608
|
background: boolean;
|
|
609
|
+
/** Null means the batch was intentionally uncapped; otherwise the batch limit. */
|
|
610
|
+
parallelLimit: number | null;
|
|
576
611
|
/** Background delivery: one message per child, or one per batch. */
|
|
577
612
|
notify: "each" | "batch";
|
|
578
613
|
status: AgentState;
|
|
@@ -611,6 +646,8 @@ export interface DispatchOptions {
|
|
|
611
646
|
signal?: AbortSignal;
|
|
612
647
|
/** A child settled (progress updates). */
|
|
613
648
|
onSettled?: (record: AgentRecord) => void;
|
|
649
|
+
/** A child was queued, started, updated, or settled. */
|
|
650
|
+
onChanged?: () => void;
|
|
614
651
|
/** Explicit wall-clock limit per child (none by default). */
|
|
615
652
|
timeoutMs?: number;
|
|
616
653
|
/** Workspace the children run in. */
|
|
@@ -627,6 +664,7 @@ export class AgentManager {
|
|
|
627
664
|
private readonly runner: AgentRunner;
|
|
628
665
|
private readonly now: () => number;
|
|
629
666
|
private readonly onDeliverable: () => void;
|
|
667
|
+
private readonly onChanged: () => void;
|
|
630
668
|
private readonly bySession = new Map<string, AgentRecord[]>();
|
|
631
669
|
private waiters: Array<{ records: AgentRecord[]; resolve: () => void }> = [];
|
|
632
670
|
private changeWaiters: Array<() => void> = [];
|
|
@@ -634,10 +672,11 @@ export class AgentManager {
|
|
|
634
672
|
private nextAgent = 1;
|
|
635
673
|
private nextBatch = 1;
|
|
636
674
|
|
|
637
|
-
constructor(options: { run: AgentRunner; now?: () => number; onDeliverable?: () => void }) {
|
|
675
|
+
constructor(options: { run: AgentRunner; now?: () => number; onDeliverable?: () => void; onChanged?: () => void }) {
|
|
638
676
|
this.runner = options.run;
|
|
639
677
|
this.now = options.now ?? Date.now;
|
|
640
678
|
this.onDeliverable = options.onDeliverable ?? (() => undefined);
|
|
679
|
+
this.onChanged = options.onChanged ?? (() => undefined);
|
|
641
680
|
}
|
|
642
681
|
|
|
643
682
|
dispatch(
|
|
@@ -661,6 +700,7 @@ export class AgentManager {
|
|
|
661
700
|
task: task.task,
|
|
662
701
|
...(task.model ? { model: task.model } : {}),
|
|
663
702
|
background: options.background,
|
|
703
|
+
parallelLimit: options.concurrency === undefined ? null : Math.max(1, Math.floor(options.concurrency)),
|
|
664
704
|
notify: options.notify ?? "batch",
|
|
665
705
|
status: "queued",
|
|
666
706
|
queuedAt: at,
|
|
@@ -679,6 +719,8 @@ export class AgentManager {
|
|
|
679
719
|
list.push(record);
|
|
680
720
|
return record;
|
|
681
721
|
});
|
|
722
|
+
this.onChanged();
|
|
723
|
+
options.onChanged?.();
|
|
682
724
|
if (options.signal) {
|
|
683
725
|
const abort = () => { void this.cancel(records); };
|
|
684
726
|
if (options.signal.aborted) abort();
|
|
@@ -697,6 +739,8 @@ export class AgentManager {
|
|
|
697
739
|
record.status = "running";
|
|
698
740
|
record.startedAt = this.now();
|
|
699
741
|
record.lastActivityAt = record.startedAt;
|
|
742
|
+
this.onChanged();
|
|
743
|
+
options.onChanged?.();
|
|
700
744
|
let result: ChildResult;
|
|
701
745
|
try {
|
|
702
746
|
result = await this.runner(task, {
|
|
@@ -709,6 +753,8 @@ export class AgentManager {
|
|
|
709
753
|
record.lastActivityAt = activity.at;
|
|
710
754
|
record.turns = activity.turns;
|
|
711
755
|
record.toolsRunning = activity.toolsRunning;
|
|
756
|
+
this.onChanged();
|
|
757
|
+
options.onChanged?.();
|
|
712
758
|
},
|
|
713
759
|
});
|
|
714
760
|
} catch (error) {
|
|
@@ -730,6 +776,9 @@ export class AgentManager {
|
|
|
730
776
|
record.finishedAt = this.now();
|
|
731
777
|
record.turns = result.turns;
|
|
732
778
|
record.toolsRunning = 0;
|
|
779
|
+
this.onChanged();
|
|
780
|
+
// The dispatch callback is not available here; settle callers also invoke
|
|
781
|
+
// their onSettled hook immediately after start() returns.
|
|
733
782
|
this.resolvers.get(record)?.(result);
|
|
734
783
|
this.resolvers.delete(record);
|
|
735
784
|
// Waiters first: what they return is delivered through their tool result.
|
|
@@ -873,6 +922,21 @@ function minutesOf(ms: number): number {
|
|
|
873
922
|
return Math.round((ms / 60_000) * 10) / 10;
|
|
874
923
|
}
|
|
875
924
|
|
|
925
|
+
/** Compact status used by both the tool stream and the TUI status bar. */
|
|
926
|
+
export function agentStatusSummary(records: AgentRecord[]): string {
|
|
927
|
+
if (records.length === 0) return "No sub-agents in this session.";
|
|
928
|
+
const running = records.filter((record) => record.status === "running").length;
|
|
929
|
+
const queued = records.filter((record) => record.status === "queued").length;
|
|
930
|
+
const settled = records.filter((record) => Boolean(record.result)).length;
|
|
931
|
+
const limits = [...new Set(records.map((record) => record.parallelLimit))];
|
|
932
|
+
const parallel = limits.length === 1 && limits[0] === null
|
|
933
|
+
? "unlimited"
|
|
934
|
+
: limits.length === 1
|
|
935
|
+
? String(limits[0])
|
|
936
|
+
: "per-batch";
|
|
937
|
+
return `${records.length} sub-agents: ${running} running, ${queued} queued, ${settled} settled (parallel: ${parallel})`;
|
|
938
|
+
}
|
|
939
|
+
|
|
876
940
|
/** One status line per sub-agent (agents_status, /agents). */
|
|
877
941
|
export function renderAgentStatus(records: AgentRecord[], now: number = Date.now()): string {
|
|
878
942
|
if (records.length === 0) return "No sub-agents in this session.";
|
|
@@ -892,8 +956,7 @@ export function renderAgentStatus(records: AgentRecord[], now: number = Date.now
|
|
|
892
956
|
const task = record.task.length > 80 ? `${record.task.slice(0, 79)}…` : record.task;
|
|
893
957
|
return `- ${bits.join(" ")} (${detail.join(", ")}) — ${task}`;
|
|
894
958
|
});
|
|
895
|
-
|
|
896
|
-
return `${records.length} sub-agents, ${running} running:\n${lines.join("\n")}`;
|
|
959
|
+
return `${agentStatusSummary(records)}:\n${lines.join("\n")}`;
|
|
897
960
|
}
|
|
898
961
|
|
|
899
962
|
/** The message that brings finished background agents back to the parent. */
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
AGENT_ROLES,
|
|
41
41
|
AGENTS_HOOK,
|
|
42
42
|
AgentManager,
|
|
43
|
+
agentStatusSummary,
|
|
43
44
|
type AgentRunner,
|
|
44
45
|
type AgentsHook,
|
|
45
46
|
type AgentRecord,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
renderAgentStatus,
|
|
48
49
|
renderChildResults,
|
|
49
50
|
renderDelivery,
|
|
51
|
+
modelSelectionFromContext,
|
|
50
52
|
runChildAgent,
|
|
51
53
|
type ChildResult,
|
|
52
54
|
type ChildTask,
|
|
@@ -66,7 +68,7 @@ const SpawnAgentsParams = Type.Object({
|
|
|
66
68
|
}),
|
|
67
69
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
68
70
|
model: Type.Optional(Type.String({
|
|
69
|
-
description: '
|
|
71
|
+
description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "gpt-6-sol:high"). Omit (or leave empty) to inherit the parent session\'s current model and effort',
|
|
70
72
|
})),
|
|
71
73
|
}),
|
|
72
74
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
@@ -99,7 +101,7 @@ function renderSome(records: AgentRecord[], unknown: string[] = []): string {
|
|
|
99
101
|
const open = records.filter((record) => !record.result);
|
|
100
102
|
const parts: string[] = [];
|
|
101
103
|
if (finished.length > 0) parts.push(renderChildResults(finished.map((record) => record.result as ChildResult), finished.map((record) => record.id)));
|
|
102
|
-
if (open.length > 0) parts.push(`
|
|
104
|
+
if (open.length > 0) parts.push(`Open sub-agents:\n${renderAgentStatus(open)}`);
|
|
103
105
|
if (unknown.length > 0) parts.push(`Unknown agent ids: ${unknown.join(", ")}`);
|
|
104
106
|
return parts.join("\n\n") || "No matching sub-agents.";
|
|
105
107
|
}
|
|
@@ -109,10 +111,27 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
109
111
|
/** The session this runtime is on (deliveries go to it). */
|
|
110
112
|
let currentSession = "";
|
|
111
113
|
let cwd = process.cwd();
|
|
114
|
+
let inheritedModel = "";
|
|
115
|
+
let lastContext: any = null;
|
|
116
|
+
let statusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
112
117
|
/** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
|
|
113
118
|
let busy = false;
|
|
114
119
|
let oneShot = false;
|
|
115
120
|
|
|
121
|
+
function publishStatus(): void {
|
|
122
|
+
statusTimer = null;
|
|
123
|
+
const session = currentSession;
|
|
124
|
+
if (!session || !lastContext?.ui?.setStatus) return;
|
|
125
|
+
const records = manager.list(session);
|
|
126
|
+
if (records.length > 0) lastContext.ui.setStatus("omnirush-agents", agentStatusSummary(records));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scheduleStatus(): void {
|
|
130
|
+
if (statusTimer) return;
|
|
131
|
+
statusTimer = setTimeout(publishStatus, 250);
|
|
132
|
+
statusTimer.unref?.();
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
const send = (records: AgentRecord[]) => {
|
|
117
136
|
pi.sendMessage(
|
|
118
137
|
{
|
|
@@ -146,6 +165,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
146
165
|
})),
|
|
147
166
|
// After the settle's own bookkeeping (waiters took theirs).
|
|
148
167
|
onDeliverable: () => queueMicrotask(deliver),
|
|
168
|
+
onChanged: scheduleStatus,
|
|
149
169
|
});
|
|
150
170
|
|
|
151
171
|
const hook: AgentsHook = {
|
|
@@ -158,7 +178,13 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
158
178
|
|
|
159
179
|
const sessionOf = (ctx: any): string => {
|
|
160
180
|
const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
161
|
-
if (id
|
|
181
|
+
if (id && id !== currentSession) {
|
|
182
|
+
currentSession = id;
|
|
183
|
+
inheritedModel = "";
|
|
184
|
+
}
|
|
185
|
+
lastContext = ctx ?? lastContext;
|
|
186
|
+
const selected = modelSelectionFromContext(ctx);
|
|
187
|
+
if (selected) inheritedModel = selected;
|
|
162
188
|
if (ctx?.cwd) cwd = String(ctx.cwd);
|
|
163
189
|
if (ctx?.mode === "print" || ctx?.mode === "json") oneShot = true;
|
|
164
190
|
return id;
|
|
@@ -190,21 +216,22 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
190
216
|
parameters: SpawnAgentsParams,
|
|
191
217
|
|
|
192
218
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
219
|
+
const parentSessionId = sessionOf(ctx);
|
|
193
220
|
const tasks: ChildTask[] = [];
|
|
194
221
|
for (const raw of params.tasks ?? []) {
|
|
195
222
|
if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
|
|
196
223
|
throw new Error("invalid tasks: every entry needs a non-empty task string");
|
|
197
224
|
}
|
|
198
|
-
// An
|
|
199
|
-
|
|
225
|
+
// An omitted model inherits the live parent selection. Explicit task
|
|
226
|
+
// models remain available for deliberate mixed-model swarms.
|
|
227
|
+
const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
200
228
|
tasks.push({
|
|
201
229
|
role: raw.role,
|
|
202
230
|
task: raw.task.trim(),
|
|
203
|
-
...(
|
|
231
|
+
...((requestedModel || inheritedModel) ? { model: requestedModel || inheritedModel } : {}),
|
|
204
232
|
});
|
|
205
233
|
}
|
|
206
234
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
207
|
-
const parentSessionId = sessionOf(ctx);
|
|
208
235
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
209
236
|
const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
210
237
|
// Only a deliberate limit counts: models fill optional numbers with
|
|
@@ -212,26 +239,33 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
212
239
|
const timeoutMs = typeof params.timeout_minutes === "number" && params.timeout_minutes >= MIN_TIMEOUT_MINUTES
|
|
213
240
|
? params.timeout_minutes * 60_000
|
|
214
241
|
: undefined;
|
|
215
|
-
// No cap: every task runs at once unless the caller
|
|
216
|
-
|
|
242
|
+
// No cap: every task runs at once unless the caller deliberately
|
|
243
|
+
// supplies max_parallel.
|
|
244
|
+
const hasParallelLimit = typeof params.max_parallel === "number" && params.max_parallel >= 1;
|
|
245
|
+
const concurrency = hasParallelLimit
|
|
217
246
|
? Math.floor(params.max_parallel)
|
|
218
|
-
:
|
|
247
|
+
: undefined;
|
|
219
248
|
const background = params.wait === false;
|
|
220
249
|
|
|
221
|
-
let settledCount = 0;
|
|
222
250
|
const dispatched = manager.dispatch(parentSessionId, tasks, {
|
|
223
251
|
background,
|
|
224
252
|
concurrency,
|
|
225
253
|
notify: params.notify === "each" ? "each" : "batch",
|
|
226
254
|
...(background ? {} : { signal }),
|
|
227
255
|
onSettled: (record) => {
|
|
228
|
-
settledCount += 1;
|
|
229
256
|
if (background) return;
|
|
230
257
|
onUpdate?.({
|
|
231
|
-
content: [{ type: "text", text:
|
|
258
|
+
content: [{ type: "text", text: `${agentStatusSummary(dispatched.records)} (latest: ${record.id} ${record.role})` }],
|
|
232
259
|
details: { results: dispatched.records.filter((candidate) => candidate.result).map(resultOf) },
|
|
233
260
|
});
|
|
234
261
|
},
|
|
262
|
+
onChanged: () => {
|
|
263
|
+
if (background) return;
|
|
264
|
+
onUpdate?.({
|
|
265
|
+
content: [{ type: "text", text: agentStatusSummary(manager.list(parentSessionId)) }],
|
|
266
|
+
details: { results: manager.list(parentSessionId).filter((candidate) => candidate.result).map(resultOf) },
|
|
267
|
+
});
|
|
268
|
+
},
|
|
235
269
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
236
270
|
cwd: workdir,
|
|
237
271
|
});
|
|
@@ -260,6 +294,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
260
294
|
task: record.task,
|
|
261
295
|
session_id: record.sessionId,
|
|
262
296
|
...(record.model ? { model: record.model } : {}),
|
|
297
|
+
parallel_limit: record.parallelLimit,
|
|
263
298
|
status: "running",
|
|
264
299
|
})),
|
|
265
300
|
},
|
|
@@ -286,7 +321,20 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
|
|
|
286
321
|
const text = renderAgentStatus(records) + (unknown.length ? `\nUnknown agent ids: ${unknown.join(", ")}` : "");
|
|
287
322
|
return {
|
|
288
323
|
content: [{ type: "text", text }],
|
|
289
|
-
details: {
|
|
324
|
+
details: {
|
|
325
|
+
agents: records.map((record) => ({
|
|
326
|
+
agent_id: record.id,
|
|
327
|
+
status: record.status,
|
|
328
|
+
role: record.role,
|
|
329
|
+
task: record.task,
|
|
330
|
+
session_id: record.sessionId,
|
|
331
|
+
...(record.model ? { model: record.model } : {}),
|
|
332
|
+
parallel_limit: record.parallelLimit,
|
|
333
|
+
turns: record.turns,
|
|
334
|
+
tools_running: record.toolsRunning,
|
|
335
|
+
last_activity_at: record.lastActivityAt,
|
|
336
|
+
})),
|
|
337
|
+
},
|
|
290
338
|
};
|
|
291
339
|
},
|
|
292
340
|
});
|