omnirush 0.10.2 → 0.10.4
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/extensions/omnirush/agents-lib.ts +59 -9
- package/assets/extensions/omnirush/agents.ts +78 -14
- package/assets/extensions/omnirush/secret-env.ts +46 -2
- package/assets/extensions/omnirush/session-sync.ts +80 -4
- package/assets/extensions/omnirush/subagents-lib.ts +113 -13
- package/assets/extensions/omnirush/subagents.ts +13 -2
- package/package.json +1 -1
- package/scripts/brand-engine.js +58 -4
- package/src/bin.js +23 -4
- package/src/compat.js +65 -0
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
ENV_FALLBACK_MODEL,
|
|
36
36
|
FALLBACK_MARKER,
|
|
37
37
|
fallbackNote,
|
|
38
|
+
sourceNote,
|
|
38
39
|
piThinking,
|
|
39
40
|
type SubagentFallback,
|
|
40
41
|
} from "./subagents-lib";
|
|
@@ -198,11 +199,18 @@ export const ROLE_PRESETS: Record<AgentRole, RolePreset> = {
|
|
|
198
199
|
},
|
|
199
200
|
};
|
|
200
201
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the child entry: the core's entry as the launcher started it
|
|
204
|
+
* (OMNIRUSH_CORE_ENTRY: through <package>/engine, so process listings name
|
|
205
|
+
* omnirush; the runtime reports the resolved path in argv[1]), else this
|
|
206
|
+
* running script, with the current runtime.
|
|
207
|
+
*/
|
|
208
|
+
export function childInvocation(args: string[], env: NodeJS.ProcessEnv = process.env): {
|
|
203
209
|
command: string;
|
|
204
210
|
args: string[];
|
|
205
211
|
} {
|
|
212
|
+
const launched = (env.OMNIRUSH_CORE_ENTRY || "").trim();
|
|
213
|
+
if (launched && existsSync(launched)) return { command: process.execPath, args: [launched, ...args] };
|
|
206
214
|
const currentScript = process.argv[1];
|
|
207
215
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
208
216
|
if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
|
|
@@ -213,8 +221,8 @@ export function childInvocation(args: string[]): {
|
|
|
213
221
|
if (!isGenericRuntime) {
|
|
214
222
|
return { command: process.execPath, args };
|
|
215
223
|
}
|
|
216
|
-
// Packaged bun builds: fall back to the
|
|
217
|
-
return { command: "
|
|
224
|
+
// Packaged bun builds: fall back to the omnirush on PATH (it forwards core flags).
|
|
225
|
+
return { command: "omnirush", args };
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
/** The folder, inside a session dir, that holds its sub-agents' sessions. */
|
|
@@ -305,8 +313,8 @@ export interface ChildTask {
|
|
|
305
313
|
role: AgentRole;
|
|
306
314
|
task: string;
|
|
307
315
|
/** Cross-model children: gateway model id the child runs on
|
|
308
|
-
* (e.g. "muse-spark-1.
|
|
309
|
-
*
|
|
316
|
+
* (e.g. "muse-spark-1.1" workers under an astra parent). Undefined =
|
|
317
|
+
* inherit the parent's model. */
|
|
310
318
|
model?: string;
|
|
311
319
|
/** The effort the child runs on (gateway spelling); undefined = pi's default. */
|
|
312
320
|
effort?: string;
|
|
@@ -316,6 +324,21 @@ export interface ChildTask {
|
|
|
316
324
|
gatewayFallback?: { model: string; effort: string | null };
|
|
317
325
|
/** Extra environment for the child (the sub-agent setting and main model, for nested layers). */
|
|
318
326
|
env?: Record<string, string>;
|
|
327
|
+
/** The model[:effort] the delegating agent asked for (the task's own `model`), when it named one. */
|
|
328
|
+
requestedModel?: string;
|
|
329
|
+
/** That model did not run: the user's /subagents choice won, or the gateway does not serve it. */
|
|
330
|
+
override?: ChildModelOverride;
|
|
331
|
+
/** Where `model` comes from ("subagents", "user_prompt", "task", "parent"). */
|
|
332
|
+
modelSource?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A task model that did not run, and what ran instead (the result's model_override). */
|
|
336
|
+
export interface ChildModelOverride {
|
|
337
|
+
requested: string;
|
|
338
|
+
used: string;
|
|
339
|
+
effort: string | null;
|
|
340
|
+
reason: string;
|
|
341
|
+
note: string;
|
|
319
342
|
}
|
|
320
343
|
|
|
321
344
|
/** A sub-agent that ran on the main model instead of the picked one. */
|
|
@@ -342,6 +365,12 @@ export interface ChildResult {
|
|
|
342
365
|
effort?: string;
|
|
343
366
|
/** It ran on the main model instead of the picked one (why, and since when). */
|
|
344
367
|
model_fallback?: ChildModelFallback;
|
|
368
|
+
/** The task's own model did not run (the user's /subagents choice, or not served). */
|
|
369
|
+
model_override?: ChildModelOverride;
|
|
370
|
+
/** Where the model comes from: "subagents", "user_prompt", "task" or "parent". */
|
|
371
|
+
model_source?: string;
|
|
372
|
+
/** The model[:effort] the task asked for, when it named one. */
|
|
373
|
+
requested_model?: string;
|
|
345
374
|
status: ChildStatus;
|
|
346
375
|
/** Process exit code (null when killed by a signal or still unknown). */
|
|
347
376
|
exitCode: number | null;
|
|
@@ -412,6 +441,9 @@ export function buildChildResult(
|
|
|
412
441
|
...(task.model ? { model: task.model } : {}),
|
|
413
442
|
...(task.effort ? { effort: task.effort } : {}),
|
|
414
443
|
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
444
|
+
...(task.override ? { model_override: { ...task.override } } : {}),
|
|
445
|
+
...(task.modelSource ? { model_source: task.modelSource } : {}),
|
|
446
|
+
...(task.requestedModel ? { requested_model: task.requestedModel } : {}),
|
|
415
447
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
416
448
|
exitCode: input.exitCode,
|
|
417
449
|
output: capped,
|
|
@@ -921,16 +953,34 @@ export async function mapWithConcurrency<TIn, TOut>(
|
|
|
921
953
|
return results;
|
|
922
954
|
}
|
|
923
955
|
|
|
956
|
+
/** The model and effort a child really ran on (after a gateway fallback), and why that one. */
|
|
957
|
+
export function ranOn(result: Pick<ChildResult, "model" | "effort" | "model_fallback">): { model: string | null; effort: string | null } {
|
|
958
|
+
if (result.model_fallback?.kind === "gateway") return { model: result.model_fallback.used, effort: result.model_fallback.effort ?? null };
|
|
959
|
+
return { model: result.model ?? null, effort: result.effort ?? null };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function ranOnText(result: ChildResult): string {
|
|
963
|
+
const { model, effort } = ranOn(result);
|
|
964
|
+
if (!model) return "the parent's model (not an omnirush.ai model)";
|
|
965
|
+
const why = result.model_fallback?.kind === "gateway"
|
|
966
|
+
? "the gateway refused the chosen model"
|
|
967
|
+
: result.model_fallback
|
|
968
|
+
? "the main model"
|
|
969
|
+
: sourceNote(result.model_source);
|
|
970
|
+
return `${model}${effort ? ` · ${effort}` : ""}${why ? ` (${why})` : ""}`;
|
|
971
|
+
}
|
|
972
|
+
|
|
924
973
|
/** Structured result text for the parent model (one section per child). */
|
|
925
974
|
export function renderChildResults(results: ChildResult[], ids?: readonly string[]): string {
|
|
926
975
|
const succeeded = results.filter((result) => result.status === "completed").length;
|
|
927
976
|
const sections = results.map((result, index) => {
|
|
928
977
|
const minutes = Math.round((result.durationMs / 60_000) * 10) / 10;
|
|
929
|
-
const
|
|
930
|
-
const
|
|
931
|
-
const label = ranOn ? ` [${ranOn}${effort ? ` · ${effort}` : ""}]` : "";
|
|
978
|
+
const ran = ranOn(result);
|
|
979
|
+
const label = ran.model ? ` [${ran.model}${ran.effort ? ` · ${ran.effort}` : ""}]` : "";
|
|
932
980
|
const header = `### ${ids?.[index] ? `${ids[index]} ` : ""}${result.role}${label} — ${result.status} (${minutes} min${result.outputTruncated ? ", output capped" : ""})`;
|
|
933
981
|
const meta: string[] = [`task: ${result.task}`];
|
|
982
|
+
meta.push(`ran on: ${ranOnText(result)}`);
|
|
983
|
+
if (result.model_override) meta.push(`note: ${result.model_override.note}`);
|
|
934
984
|
if (result.model_fallback) meta.push(`note: ${result.model_fallback.note}`);
|
|
935
985
|
if (result.error) meta.push(`error: ${result.error}`);
|
|
936
986
|
return `${header}\n${meta.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from "./agents-lib";
|
|
74
74
|
import { SWARM_GUIDELINE } from "./swarm-lib";
|
|
75
75
|
import { omniDir } from "./auth";
|
|
76
|
+
import { loadCatalog, type CatalogModel } from "./subagents-lib";
|
|
76
77
|
import { MemoryAdmission, readMemorySettings } from "./memory-lib";
|
|
77
78
|
|
|
78
79
|
/** Blocking spawn_agents: progress updates at most this often (ms). */
|
|
@@ -84,16 +85,32 @@ const MIN_TIMEOUT_MINUTES = 1;
|
|
|
84
85
|
/** customType of the message that brings background results back. */
|
|
85
86
|
export const DELIVERY_TYPE = "omnirush-agents-result";
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
/** Served ids used as examples when the account's catalog is not cached (the live list at 0.10.3). */
|
|
89
|
+
export const SERVED_MODEL_EXAMPLES = ["gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol", "meta-muse-spark", "muse-spark-1.1"];
|
|
90
|
+
|
|
91
|
+
/** Examples for the per-task model: the ids the account's live catalog lists (served models only). */
|
|
92
|
+
export function modelExamples(catalog: readonly CatalogModel[] | null): string[] {
|
|
93
|
+
if (!catalog || catalog.length === 0) return SERVED_MODEL_EXAMPLES;
|
|
94
|
+
return catalog.map((model) => model.id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function modelParamDescription(examples: readonly string[]): string {
|
|
98
|
+
return [
|
|
99
|
+
"Optional model[:effort] for THIS task. Usually omit it: set it only when the user asked for a model for the sub-agents.",
|
|
100
|
+
`Served models: ${examples.map((id) => `"${id}"`).join(", ")}, optionally with an effort (e.g. "${examples[0] ?? "gpt-6-astra"}:high"); a model the account is not served is never used (the child runs on your model and effort).`,
|
|
101
|
+
"The user's /subagents choice always wins: while it is set, a model named here runs only if the user's own message names it.",
|
|
102
|
+
"Omitted, the child runs on the /subagents choice, else this session's current model and effort.",
|
|
103
|
+
].join(" ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const spawnAgentsParams = (examples: readonly string[]) => Type.Object({
|
|
88
107
|
tasks: Type.Array(
|
|
89
108
|
Type.Object({
|
|
90
109
|
role: StringEnum(AGENT_ROLES, {
|
|
91
110
|
description: "code-searcher: read-only codebase exploration; researcher-web: web research via web_search/web_fetch; general-worker: full-tool implementation work",
|
|
92
111
|
}),
|
|
93
112
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
94
|
-
model: Type.Optional(Type.String({
|
|
95
|
-
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',
|
|
96
|
-
})),
|
|
113
|
+
model: Type.Optional(Type.String({ description: modelParamDescription(examples) })),
|
|
97
114
|
}),
|
|
98
115
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
99
116
|
),
|
|
@@ -115,6 +132,26 @@ const IdsParam = Type.Optional(Type.Array(Type.String(), {
|
|
|
115
132
|
description: 'Agent ids from spawn_agents (e.g. "a3"); omit (or "all") for every agent of this session',
|
|
116
133
|
}));
|
|
117
134
|
|
|
135
|
+
/** The text of the latest user message on the session's branch (null when there is none). */
|
|
136
|
+
export function lastUserMessageText(ctx: any): string | null {
|
|
137
|
+
let entries: any[] = [];
|
|
138
|
+
try {
|
|
139
|
+
entries = ctx?.sessionManager?.getBranch?.() ?? ctx?.sessionManager?.getEntries?.() ?? [];
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
144
|
+
const message = entries[index]?.type === "message" ? entries[index].message : null;
|
|
145
|
+
if (!message || message.role !== "user") continue;
|
|
146
|
+
if (typeof message.content === "string") return message.content;
|
|
147
|
+
if (Array.isArray(message.content)) {
|
|
148
|
+
return message.content.filter((part: any) => part?.type === "text" && typeof part.text === "string").map((part: any) => part.text).join("\n");
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
function resultOf(record: AgentRecord): ChildResult & { agent_id: string } {
|
|
119
156
|
return { ...(record.result as ChildResult), agent_id: record.id };
|
|
120
157
|
}
|
|
@@ -227,6 +264,30 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
227
264
|
};
|
|
228
265
|
(globalThis as any)[AGENTS_HOOK] = hook;
|
|
229
266
|
|
|
267
|
+
// The user's latest message (the text they typed, not an extension's):
|
|
268
|
+
// a task's own model runs over the /subagents choice only when it names
|
|
269
|
+
// that model. Sub-agents have no user: their task text never counts.
|
|
270
|
+
let lastUserInput: { session: string; text: string } | null = null;
|
|
271
|
+
const sessionIdOf = (ctx: any): string => {
|
|
272
|
+
try {
|
|
273
|
+
return String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
274
|
+
} catch {
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
if (depth === 0) {
|
|
279
|
+
pi.on("input", (event: any, ctx: any) => {
|
|
280
|
+
if (event?.source === "extension" || typeof event?.text !== "string") return;
|
|
281
|
+
lastUserInput = { session: sessionIdOf(ctx), text: event.text };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const latestUserPrompt = (ctx: any): string | null => {
|
|
285
|
+
if (depth > 0) return null;
|
|
286
|
+
const session = sessionIdOf(ctx);
|
|
287
|
+
if (lastUserInput && lastUserInput.session === session) return lastUserInput.text;
|
|
288
|
+
return lastUserMessageText(ctx);
|
|
289
|
+
};
|
|
290
|
+
|
|
230
291
|
const sessionOf = (ctx: any): string => {
|
|
231
292
|
lastContext = ctx ?? lastContext;
|
|
232
293
|
return hub.sessionOf(ctx);
|
|
@@ -244,7 +305,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
244
305
|
"Default (wait true) blocks until all children finish and returns their results.",
|
|
245
306
|
"wait:false dispatches them in the BACKGROUND: the call returns at once with agent ids, you keep working, and their results arrive automatically as a message in this conversation when they finish.",
|
|
246
307
|
"Manage background agents with agents_status, agents_wait, agents_result and agents_cancel.",
|
|
247
|
-
'Each task can run on a
|
|
308
|
+
'Each task can run on a different model via its "model" field when the user asks for one; the user\'s /subagents choice wins over it, and a model the account is not served is not used. Every result says the model and effort the child ran on.',
|
|
248
309
|
"Every task description must be SELF-CONTAINED: the child cannot see this conversation.",
|
|
249
310
|
"Use for parallelizable work: broad code surveys, independent research questions, independent implementation chunks.",
|
|
250
311
|
"Give each task only its own part of the work: never the user's whole message or instructions about sub-agents (the child would follow them and delegate again).",
|
|
@@ -257,7 +318,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
257
318
|
"Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
|
|
258
319
|
...(depth > 0 ? [] : [SWARM_GUIDELINE]),
|
|
259
320
|
],
|
|
260
|
-
parameters:
|
|
321
|
+
parameters: spawnAgentsParams(modelExamples(loadCatalog(omniDir()))),
|
|
261
322
|
|
|
262
323
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
263
324
|
const parentSessionId = sessionOf(ctx);
|
|
@@ -266,9 +327,9 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
266
327
|
if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
|
|
267
328
|
throw new Error("invalid tasks: every entry needs a non-empty task string");
|
|
268
329
|
}
|
|
269
|
-
// A task's own model[:effort]
|
|
270
|
-
// the /subagents
|
|
271
|
-
//
|
|
330
|
+
// A task's own model[:effort] (resolved below by subagentTasks):
|
|
331
|
+
// the user's /subagents choice wins over it unless the user's message
|
|
332
|
+
// names that model; unset, it runs only when the gateway serves it.
|
|
272
333
|
const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
273
334
|
tasks.push({
|
|
274
335
|
role: raw.role,
|
|
@@ -277,11 +338,11 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
277
338
|
});
|
|
278
339
|
}
|
|
279
340
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
280
|
-
// The model and effort each child runs on (subagents.ts): the
|
|
281
|
-
//
|
|
341
|
+
// The model and effort each child runs on (subagents.ts): the
|
|
342
|
+
// /subagents choice, else a served task model, else the parent
|
|
282
343
|
// session's current model and effort; the main model when a pick is
|
|
283
|
-
// unavailable.
|
|
284
|
-
const resolvedTasks = subagentTasks(pi, ctx, tasks);
|
|
344
|
+
// unavailable. Overrides are noted on the result and the trace.
|
|
345
|
+
const resolvedTasks = subagentTasks(pi, ctx, tasks, { userPrompt: latestUserPrompt(ctx) });
|
|
285
346
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
286
347
|
const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
287
348
|
// Only a deliberate limit counts: models fill optional numbers with
|
|
@@ -334,7 +395,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
334
395
|
|
|
335
396
|
if (background) {
|
|
336
397
|
const lines = dispatched.records.map((record, index) => {
|
|
337
|
-
const note = childModelFallback(resolvedTasks[index])?.note;
|
|
398
|
+
const note = [resolvedTasks[index].override?.note, childModelFallback(resolvedTasks[index])?.note].filter(Boolean).join("; ");
|
|
338
399
|
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})` : ""}`;
|
|
339
400
|
});
|
|
340
401
|
return {
|
|
@@ -363,6 +424,9 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admissi
|
|
|
363
424
|
...(record.model ? { model: record.model } : {}),
|
|
364
425
|
...(record.effort ? { effort: record.effort } : {}),
|
|
365
426
|
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
427
|
+
...(resolvedTasks[index].override ? { model_override: { ...resolvedTasks[index].override } } : {}),
|
|
428
|
+
...(resolvedTasks[index].modelSource ? { model_source: resolvedTasks[index].modelSource } : {}),
|
|
429
|
+
...(resolvedTasks[index].requestedModel ? { requested_model: resolvedTasks[index].requestedModel } : {}),
|
|
366
430
|
parallel_limit: record.parallelLimit,
|
|
367
431
|
status: "running",
|
|
368
432
|
};
|
|
@@ -5,11 +5,55 @@ const SECRET_ENV_NAMES = new Set([
|
|
|
5
5
|
"OMNIRUSH_APPROVAL_TOKEN",
|
|
6
6
|
]);
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The agent core's own PI_<NAME> variables (src/compat.js
|
|
10
|
+
* CORE_ONLY_ENV_NAMES; a test keeps the lists equal). Shell commands and
|
|
11
|
+
* MCP servers see their OMNIRUSH_<NAME> copies only; any other PI_*
|
|
12
|
+
* variable (a user's own) passes through.
|
|
13
|
+
*/
|
|
14
|
+
export const CORE_ONLY_ENV_NAMES = [
|
|
15
|
+
"CODING_AGENT",
|
|
16
|
+
"CODING_AGENT_DIR",
|
|
17
|
+
"CODING_AGENT_SESSION_DIR",
|
|
18
|
+
"SESSION_ID",
|
|
19
|
+
"SESSION_FILE",
|
|
20
|
+
"PROVIDER",
|
|
21
|
+
"MODEL",
|
|
22
|
+
"REASONING_LEVEL",
|
|
23
|
+
"OFFLINE",
|
|
24
|
+
"TELEMETRY",
|
|
25
|
+
"INSTALLER_API_BASE",
|
|
26
|
+
"MANAGED_INSTALL_ROOT",
|
|
27
|
+
"CACHE_RETENTION",
|
|
28
|
+
"CLEAR_ON_SHRINK",
|
|
29
|
+
"EXPERIMENTAL",
|
|
30
|
+
"HARDWARE_CURSOR",
|
|
31
|
+
"HYPERLINKS",
|
|
32
|
+
"IMAGE_PROTOCOL",
|
|
33
|
+
"OAUTH_CALLBACK_HOST",
|
|
34
|
+
"PACKAGE_DIR",
|
|
35
|
+
"RADIUS_GATEWAY",
|
|
36
|
+
"SHARE_VIEWER_URL",
|
|
37
|
+
"SKIP_VERSION_CHECK",
|
|
38
|
+
"STARTUP_BENCHMARK",
|
|
39
|
+
"TIMING",
|
|
40
|
+
"TRUE_COLOR",
|
|
41
|
+
"TUI_DEBUG",
|
|
42
|
+
"TUI_DEBUG_REDRAW",
|
|
43
|
+
"TUI_ESC_TIMEOUT",
|
|
44
|
+
"TUI_WRITE_LOG",
|
|
45
|
+
];
|
|
46
|
+
const CORE_ONLY_ENV = new Set(CORE_ONLY_ENV_NAMES.map((name) => `PI_${name}`));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keep provider credentials in the agent process, never in shell/MCP
|
|
50
|
+
* children; the core's PI_* variables stay there too (the children get the
|
|
51
|
+
* OMNIRUSH_* spellings).
|
|
52
|
+
*/
|
|
9
53
|
export function sanitizeToolEnvironment(input: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
10
54
|
const output: NodeJS.ProcessEnv = {};
|
|
11
55
|
for (const [key, value] of Object.entries(input)) {
|
|
12
|
-
if (!SECRET_ENV_NAMES.has(key) && value !== undefined) output[key] = value;
|
|
56
|
+
if (!SECRET_ENV_NAMES.has(key) && !CORE_ONLY_ENV.has(key) && value !== undefined) output[key] = value;
|
|
13
57
|
}
|
|
14
58
|
return output;
|
|
15
59
|
}
|
|
@@ -159,13 +159,48 @@ function modelFallbackOf(result: any): ChildModelFallback | null {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** A task's own model that did not run (spawn_agents' model_override). */
|
|
163
|
+
export type ChildModelOverride = { requested: string; used: string; effort: string | null; reason: string; note: string };
|
|
164
|
+
|
|
165
|
+
/** The model and effort a sub-agent really ran on, once its result is final. */
|
|
166
|
+
export type ChildRanOn = { model: string | null; effort: string | null; source: string | null; requested: string | null; status: string };
|
|
167
|
+
|
|
168
|
+
function modelOverrideOf(result: any): ChildModelOverride | null {
|
|
169
|
+
const override = result && typeof result.model_override === "object" ? result.model_override : null;
|
|
170
|
+
if (!override || typeof override.requested !== "string" || typeof override.used !== "string") return null;
|
|
171
|
+
return {
|
|
172
|
+
requested: override.requested,
|
|
173
|
+
used: override.used,
|
|
174
|
+
effort: typeof override.effort === "string" ? override.effort : null,
|
|
175
|
+
reason: typeof override.reason === "string" ? override.reason : "user_choice",
|
|
176
|
+
note: typeof override.note === "string" ? override.note : `ran on ${override.used}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A finished child's model and effort (the gateway fallback's when the gateway moved it). */
|
|
181
|
+
function ranOnOf(result: any, fallback: ChildModelFallback | null): ChildRanOn | null {
|
|
182
|
+
const status = result && typeof result.status === "string" ? result.status : "";
|
|
183
|
+
if (!status || status === "running" || status === "queued") return null;
|
|
184
|
+
const gateway = fallback?.kind === "gateway" ? fallback : null;
|
|
185
|
+
return {
|
|
186
|
+
model: gateway ? gateway.used : typeof result.model === "string" ? result.model : null,
|
|
187
|
+
effort: gateway ? gateway.effort : typeof result.effort === "string" ? result.effort : null,
|
|
188
|
+
source: typeof result.model_source === "string" ? result.model_source : null,
|
|
189
|
+
requested: typeof result.requested_model === "string" ? result.requested_model : null,
|
|
190
|
+
status,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type SpawnedChild = { id: string; role: string | null; task: string | null; modelFallback?: ChildModelFallback; modelOverride?: ChildModelOverride; ranOn?: ChildRanOn };
|
|
195
|
+
|
|
162
196
|
/**
|
|
163
197
|
* Sub-agents a session started: its spawn_agents results (session id, role,
|
|
164
198
|
* task), with the model fallback a later result reported (a background
|
|
165
|
-
* child's delivery, agents_wait / agents_result) when there is one
|
|
199
|
+
* child's delivery, agents_wait / agents_result) when there is one, a task
|
|
200
|
+
* model that did not run, and the model and effort it ran on once finished.
|
|
166
201
|
*/
|
|
167
|
-
export function spawnedChildren(entries: readonly PiEntry[]):
|
|
168
|
-
const children:
|
|
202
|
+
export function spawnedChildren(entries: readonly PiEntry[]): SpawnedChild[] {
|
|
203
|
+
const children: SpawnedChild[] = [];
|
|
169
204
|
for (const entry of entries) {
|
|
170
205
|
const message = entry.type === "message" ? entry.message : null;
|
|
171
206
|
const spawned = Boolean(message && message.role === "toolResult" && message.toolName === "spawn_agents");
|
|
@@ -182,16 +217,23 @@ export function spawnedChildren(entries: readonly PiEntry[]): Array<{ id: string
|
|
|
182
217
|
if (!sanitizeSessionId(id)) continue;
|
|
183
218
|
const known = children.find((child) => child.id === id);
|
|
184
219
|
const modelFallback = modelFallbackOf(result);
|
|
220
|
+
const modelOverride = modelOverrideOf(result);
|
|
185
221
|
if (known) {
|
|
186
222
|
if (modelFallback) known.modelFallback = modelFallback;
|
|
223
|
+
if (modelOverride) known.modelOverride = modelOverride;
|
|
224
|
+
const ran = ranOnOf(result, known.modelFallback ?? null);
|
|
225
|
+
if (ran) known.ranOn = ran;
|
|
187
226
|
continue;
|
|
188
227
|
}
|
|
189
228
|
if (!spawned) continue;
|
|
229
|
+
const ran = ranOnOf(result, modelFallback);
|
|
190
230
|
children.push({
|
|
191
231
|
id,
|
|
192
232
|
role: typeof result.role === "string" ? result.role : null,
|
|
193
233
|
task: typeof result.task === "string" ? result.task : null,
|
|
194
234
|
...(modelFallback ? { modelFallback } : {}),
|
|
235
|
+
...(modelOverride ? { modelOverride } : {}),
|
|
236
|
+
...(ran ? { ranOn: ran } : {}),
|
|
195
237
|
});
|
|
196
238
|
}
|
|
197
239
|
}
|
|
@@ -200,6 +242,15 @@ export function spawnedChildren(entries: readonly PiEntry[]): Array<{ id: string
|
|
|
200
242
|
|
|
201
243
|
/** Fallbacks already recorded as trace events (root session : child session). */
|
|
202
244
|
const recordedFallbacks = new Set<string>();
|
|
245
|
+
/** Overrides and ran-on models already recorded (same keys). */
|
|
246
|
+
const recordedOverrides = new Set<string>();
|
|
247
|
+
const recordedRanOn = new Set<string>();
|
|
248
|
+
|
|
249
|
+
/** A child's trace title: its task, with the model note when its model was not the one asked for. */
|
|
250
|
+
function childTitle(child: SpawnedChild): string | null {
|
|
251
|
+
const notes = [child.modelOverride?.note, child.modelFallback?.note].filter(Boolean);
|
|
252
|
+
return child.task && notes.length ? `${child.task} (${notes.join("; ")})` : child.task;
|
|
253
|
+
}
|
|
203
254
|
|
|
204
255
|
/** The session file of `sessionId`: `<ts>_<id>.jsonl` in one of the session dirs. */
|
|
205
256
|
async function findSessionFile(sessionId: string, dirs: readonly string[]): Promise<string | null> {
|
|
@@ -369,13 +420,38 @@ export async function captureChildSessions(input: {
|
|
|
369
420
|
kind: child.modelFallback.kind,
|
|
370
421
|
});
|
|
371
422
|
}
|
|
423
|
+
if (child.modelOverride && !recordedOverrides.has(fallbackKey)) {
|
|
424
|
+
// The task named a model that did not run: the user's /subagents
|
|
425
|
+
// choice won, or the gateway does not serve it to this account.
|
|
426
|
+
recordedOverrides.add(fallbackKey);
|
|
427
|
+
input.sync.recordTrace(input.rootSessionId, "subagent.model_override", {
|
|
428
|
+
child_session_id: child.id,
|
|
429
|
+
requested: child.modelOverride.requested,
|
|
430
|
+
used: child.modelOverride.used,
|
|
431
|
+
effort: child.modelOverride.effort,
|
|
432
|
+
reason: child.modelOverride.reason,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (child.ranOn && !recordedRanOn.has(fallbackKey)) {
|
|
436
|
+
// What the sub-agent really ran on (after any gateway fallback).
|
|
437
|
+
recordedRanOn.add(fallbackKey);
|
|
438
|
+
input.sync.recordTrace(input.rootSessionId, "subagent.model", {
|
|
439
|
+
child_session_id: child.id,
|
|
440
|
+
role: child.role,
|
|
441
|
+
model: child.ranOn.model,
|
|
442
|
+
effort: child.ranOn.effort,
|
|
443
|
+
source: child.ranOn.source,
|
|
444
|
+
requested: child.ranOn.requested,
|
|
445
|
+
status: child.ranOn.status,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
372
448
|
if (history.delta.length > 0 || !input.known.has(child.id)) {
|
|
373
449
|
await input.beforeRecord(history.sizes.reduce((total, bytes) => total + bytes, 0));
|
|
374
450
|
input.sync.recordChildSession(input.rootSessionId, {
|
|
375
451
|
childSessionId: child.id,
|
|
376
452
|
parentSessionId: input.parentSessionId,
|
|
377
453
|
depth: input.depth,
|
|
378
|
-
title:
|
|
454
|
+
title: childTitle(child),
|
|
379
455
|
agent: turnModelFromMessages(messages)?.agent ?? agent,
|
|
380
456
|
messages: history.delta,
|
|
381
457
|
lastMessageId: typeof messages.at(-1)?.info.id === "string" ? (messages.at(-1)!.info.id as string) : null,
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// agent's model and the main agent's effort.
|
|
13
13
|
// - spawn_agents (agents.ts) resolves every task against it
|
|
14
14
|
// (resolveSubagentModel) and starts the child with that --model and
|
|
15
|
-
// --thinking
|
|
15
|
+
// --thinking. The user's choice wins over a task's own `model` (the
|
|
16
|
+
// agent's), which runs only when the user's message names it; with no
|
|
17
|
+
// choice set, a task model runs only when the account's catalog lists
|
|
18
|
+
// it (served), else the delegating agent's model and effort. Every
|
|
19
|
+
// override is on the result and in the trace (subagent.model_override),
|
|
20
|
+
// and every finished child's real model and effort (subagent.model).
|
|
16
21
|
// - A picked model that is not in the account's catalog (the list the
|
|
17
22
|
// launcher fetched from GET /v1/models), or that the gateway refused in
|
|
18
23
|
// the last five minutes, resolves to the main model instead (a
|
|
@@ -134,7 +139,9 @@ export function catalogFromPayload(payload: unknown): CatalogModel[] | null {
|
|
|
134
139
|
const ownership = [entry.provider, entry.owned_by, entry.owner, entry.source, entry.namespace, entry.managed_by]
|
|
135
140
|
.filter((value): value is string => typeof value === "string")
|
|
136
141
|
.map((value) => value.trim().toLowerCase());
|
|
137
|
-
|
|
142
|
+
// Ids the gateway serves under their bare names; an id counts as served
|
|
143
|
+
// only when this payload lists it (never because the CLI ships it).
|
|
144
|
+
const shipped = ["gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol", "meta-muse-spark", "muse-spark-1.1"];
|
|
138
145
|
if (!shipped.includes(id) && entry.omnirush !== true && !ownership.includes("omnirush")) continue;
|
|
139
146
|
const levels = Array.isArray(entry.reasoning_levels)
|
|
140
147
|
? EFFORTS.filter((effort) => entry.reasoning_levels.some((level: unknown) => parseEffort(level) === effort))
|
|
@@ -323,6 +330,26 @@ export interface SubagentFallback {
|
|
|
323
330
|
reason: FallbackReason | string;
|
|
324
331
|
}
|
|
325
332
|
|
|
333
|
+
/** Why a task's own `model` did not run: the user's /subagents choice, or the gateway does not serve it. */
|
|
334
|
+
export type OverrideReason = "user_choice" | "not_served" | "catalog_unavailable" | "refused";
|
|
335
|
+
|
|
336
|
+
/** A task's own `model[:effort]` that was not used, and what ran instead. */
|
|
337
|
+
export interface SubagentOverride {
|
|
338
|
+
/** The task's model[:effort] as the agent wrote it. */
|
|
339
|
+
requested: string;
|
|
340
|
+
used: string;
|
|
341
|
+
effort: string | null;
|
|
342
|
+
reason: OverrideReason;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Where a sub-agent's model comes from: "subagents" (the user's /subagents
|
|
347
|
+
* choice), "user_prompt" (a task model the user's own message named),
|
|
348
|
+
* "task" (a task model the gateway serves, /subagents unset) or "parent"
|
|
349
|
+
* (the delegating agent's model and effort).
|
|
350
|
+
*/
|
|
351
|
+
export type ModelSource = "subagents" | "user_prompt" | "task" | "parent";
|
|
352
|
+
|
|
326
353
|
export interface SubagentResolution {
|
|
327
354
|
/** The model the child runs on; null = leave the child to pi's own default (no omnirush model known). */
|
|
328
355
|
model: string | null;
|
|
@@ -332,6 +359,10 @@ export interface SubagentResolution {
|
|
|
332
359
|
fallback?: SubagentFallback;
|
|
333
360
|
/** The main model (and effort) the child's gateway guard moves to when the gateway refuses `model`. */
|
|
334
361
|
gatewayFallback?: { model: string; effort: string | null };
|
|
362
|
+
/** The task named a model that did not run (the user's choice won, or it is not served). */
|
|
363
|
+
override?: SubagentOverride;
|
|
364
|
+
/** Where the model comes from (absent when no omnirush model is known). */
|
|
365
|
+
source?: ModelSource;
|
|
335
366
|
}
|
|
336
367
|
|
|
337
368
|
export interface AgentModel {
|
|
@@ -342,14 +373,33 @@ export interface AgentModel {
|
|
|
342
373
|
effort: string | null;
|
|
343
374
|
}
|
|
344
375
|
|
|
376
|
+
/** Whether `text` names the model id `id` (case-insensitive, as a whole id: "gpt-6-sol" is not named by "gpt-6-sol-mini" or "gpt-5.6-sol"). */
|
|
377
|
+
export function textNamesModel(text: string | null | undefined, id: string): boolean {
|
|
378
|
+
if (typeof text !== "string" || !text || !id) return false;
|
|
379
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
380
|
+
return new RegExp(`(?<![A-Za-z0-9._-])${escaped}(?![A-Za-z0-9_-]|\\.[A-Za-z0-9])`, "i").test(text);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Whether the gateway serves `model` to this account: the live catalog lists it. */
|
|
384
|
+
export function isServed(catalog: readonly CatalogModel[] | null, model: string): boolean {
|
|
385
|
+
return Boolean(catalog?.some((entry) => entry.id === model));
|
|
386
|
+
}
|
|
387
|
+
|
|
345
388
|
/**
|
|
346
389
|
* What one sub-agent runs on.
|
|
347
390
|
*
|
|
348
391
|
* `delegating` is the agent calling spawn_agents (its model is what "same as
|
|
349
392
|
* main" keeps); `main` is the main session's model and effort (handed down
|
|
350
393
|
* to nested layers through the environment), the one a picked model falls
|
|
351
|
-
* back to. `explicit` is the task's own `model
|
|
352
|
-
*
|
|
394
|
+
* back to. `explicit` is the task's own `model[:effort]` (the agent's
|
|
395
|
+
* choice) and `userPrompt` the user's latest message:
|
|
396
|
+
* - the user set /subagents (model and/or effort): that choice wins; a
|
|
397
|
+
* task model runs only when the user's own message names it (and the
|
|
398
|
+
* gateway serves it), otherwise it is overridden ("user_choice");
|
|
399
|
+
* - /subagents unset: a task model runs only when the account's live
|
|
400
|
+
* catalog lists it (and the gateway did not refuse it a moment ago);
|
|
401
|
+
* otherwise the delegating agent's model and the main effort run
|
|
402
|
+
* ("not_served", "catalog_unavailable" or "refused").
|
|
353
403
|
*/
|
|
354
404
|
export function resolveSubagentModel(input: {
|
|
355
405
|
setting: SubagentSetting;
|
|
@@ -357,6 +407,7 @@ export function resolveSubagentModel(input: {
|
|
|
357
407
|
delegating: AgentModel;
|
|
358
408
|
main?: AgentModel | null;
|
|
359
409
|
explicit?: string | null;
|
|
410
|
+
userPrompt?: string | null;
|
|
360
411
|
refused?: (model: string) => boolean;
|
|
361
412
|
}): SubagentResolution {
|
|
362
413
|
const { setting, catalog, delegating } = input;
|
|
@@ -364,6 +415,7 @@ export function resolveSubagentModel(input: {
|
|
|
364
415
|
const mainIsGateway = main.provider === PROVIDER_ID && Boolean(main.model);
|
|
365
416
|
const wantedEffort = setting.effort ?? parseEffort(main.effort) ?? null;
|
|
366
417
|
const effortOn = (model: string): string | null => nearestEffort(wantedEffort, levelsFor(catalog, model));
|
|
418
|
+
const userChose = Boolean(setting.model || setting.effort);
|
|
367
419
|
|
|
368
420
|
const withGatewayFallback = (resolution: SubagentResolution): SubagentResolution => {
|
|
369
421
|
if (
|
|
@@ -377,21 +429,39 @@ export function resolveSubagentModel(input: {
|
|
|
377
429
|
return resolution;
|
|
378
430
|
};
|
|
379
431
|
|
|
380
|
-
// The task's own model[:effort]
|
|
381
|
-
|
|
382
|
-
const explicit =
|
|
432
|
+
// The task's own model[:effort].
|
|
433
|
+
const requested = typeof input.explicit === "string" ? input.explicit.trim() : "";
|
|
434
|
+
const explicit = requested && isModelId(requested) ? splitModelEffort(requested) : null;
|
|
435
|
+
let overrideReason: OverrideReason | null = null;
|
|
383
436
|
if (explicit && isModelId(explicit.model)) {
|
|
384
|
-
const
|
|
385
|
-
|
|
437
|
+
const served = isServed(catalog, explicit.model);
|
|
438
|
+
const refusedNow = served && Boolean(input.refused?.(explicit.model));
|
|
439
|
+
const named = served && textNamesModel(input.userPrompt, explicit.model);
|
|
440
|
+
const allowed = served && !refusedNow && (userChose ? named : true);
|
|
441
|
+
if (allowed) {
|
|
442
|
+
// An effort the task names applies unless the user picked one with /subagents.
|
|
443
|
+
const taskEffort = explicit.effort && !setting.effort ? explicit.effort : null;
|
|
444
|
+
const effort = taskEffort ? nearestEffort(taskEffort, levelsFor(catalog, explicit.model)) ?? taskEffort : effortOn(explicit.model);
|
|
445
|
+
return withGatewayFallback({ model: explicit.model, effort, source: named ? "user_prompt" : "task" });
|
|
446
|
+
}
|
|
447
|
+
overrideReason = userChose ? "user_choice" : !catalog ? "catalog_unavailable" : !served ? "not_served" : "refused";
|
|
386
448
|
}
|
|
449
|
+
const withOverride = (resolution: SubagentResolution): SubagentResolution => {
|
|
450
|
+
if (!overrideReason || !explicit || !resolution.model) return resolution;
|
|
451
|
+
// The same model at the same effort is no override.
|
|
452
|
+
if (explicit.model === resolution.model && (!explicit.effort || explicit.effort === resolution.effort)) return resolution;
|
|
453
|
+
resolution.override = { requested, used: resolution.model, effort: resolution.effort, reason: overrideReason };
|
|
454
|
+
return resolution;
|
|
455
|
+
};
|
|
387
456
|
|
|
388
457
|
if (setting.model) {
|
|
389
458
|
const inCatalog = !catalog || catalog.some((entry) => entry.id === setting.model);
|
|
390
459
|
const reason: FallbackReason | null = !inCatalog ? "not_in_catalog" : input.refused?.(setting.model) ? "refused" : null;
|
|
391
460
|
if (reason && mainIsGateway && main.model !== setting.model) {
|
|
392
|
-
return {
|
|
461
|
+
return withOverride({
|
|
393
462
|
model: main.model!,
|
|
394
463
|
effort: effortOn(main.model!),
|
|
464
|
+
source: "subagents",
|
|
395
465
|
fallback: {
|
|
396
466
|
requested: setting.model,
|
|
397
467
|
requestedName: displayName(catalog, setting.model),
|
|
@@ -399,16 +469,46 @@ export function resolveSubagentModel(input: {
|
|
|
399
469
|
usedName: displayName(catalog, main.model!),
|
|
400
470
|
reason,
|
|
401
471
|
},
|
|
402
|
-
};
|
|
472
|
+
});
|
|
403
473
|
}
|
|
404
|
-
return withGatewayFallback({ model: setting.model, effort: effortOn(setting.model) });
|
|
474
|
+
return withOverride(withGatewayFallback({ model: setting.model, effort: effortOn(setting.model), source: "subagents" }));
|
|
405
475
|
}
|
|
406
476
|
|
|
407
477
|
// "Same as main": the delegating agent's own model.
|
|
408
478
|
if (delegating.provider !== PROVIDER_ID || !delegating.model) {
|
|
409
479
|
return { model: null, effort: null };
|
|
410
480
|
}
|
|
411
|
-
return withGatewayFallback({
|
|
481
|
+
return withOverride(withGatewayFallback({
|
|
482
|
+
model: delegating.model,
|
|
483
|
+
effort: effortOn(delegating.model),
|
|
484
|
+
source: userChose ? "subagents" : "parent",
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** A readable note for a task model that did not run (for the parent's result and the trace title). */
|
|
489
|
+
export function overrideNote(override: SubagentOverride, catalog: readonly CatalogModel[] | null = null): string {
|
|
490
|
+
const ran = `${displayName(catalog, override.used)}${override.effort ? ` · ${override.effort}` : ""}`;
|
|
491
|
+
switch (override.reason) {
|
|
492
|
+
case "user_choice":
|
|
493
|
+
return `ran on ${ran}, your /subagents choice (${override.requested} was asked for)`;
|
|
494
|
+
case "not_served":
|
|
495
|
+
return `ran on ${ran}, the parent's model: ${override.requested} is not served to this account`;
|
|
496
|
+
case "catalog_unavailable":
|
|
497
|
+
return `ran on ${ran}, the parent's model: the account's model list is not available to check ${override.requested}`;
|
|
498
|
+
default:
|
|
499
|
+
return `ran on ${ran}, the parent's model: ${override.requested} was refused by omnirush.ai recently`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** How a sub-agent's model was chosen, in words (the result the parent reads). */
|
|
504
|
+
export function sourceNote(source: ModelSource | string | undefined): string {
|
|
505
|
+
switch (source) {
|
|
506
|
+
case "subagents": return "your /subagents choice";
|
|
507
|
+
case "user_prompt": return "named in your message";
|
|
508
|
+
case "task": return "the task's model";
|
|
509
|
+
case "parent": return "same as the parent";
|
|
510
|
+
default: return "";
|
|
511
|
+
}
|
|
412
512
|
}
|
|
413
513
|
|
|
414
514
|
/** A readable note for a sub-agent that ran on the main model instead of the picked one. */
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
loadCatalog,
|
|
32
32
|
nearestEffort,
|
|
33
33
|
nextSetting,
|
|
34
|
+
overrideNote,
|
|
34
35
|
parseEffort,
|
|
35
36
|
PROVIDER_ID,
|
|
36
37
|
readSetting,
|
|
@@ -115,7 +116,12 @@ export function mainModel(pi: any, ctx: any, env: NodeJS.ProcessEnv = process.en
|
|
|
115
116
|
* model when the picked one is not available, and the environment that hands
|
|
116
117
|
* the setting and the main model down to nested layers.
|
|
117
118
|
*/
|
|
118
|
-
export function subagentTasks(
|
|
119
|
+
export function subagentTasks(
|
|
120
|
+
pi: any,
|
|
121
|
+
ctx: any,
|
|
122
|
+
tasks: ChildTask[],
|
|
123
|
+
options: { dir?: string; env?: NodeJS.ProcessEnv; now?: number; userPrompt?: string | null } = {},
|
|
124
|
+
): ChildTask[] {
|
|
119
125
|
const dir = options.dir ?? omniDir();
|
|
120
126
|
const env = options.env ?? process.env;
|
|
121
127
|
const { setting } = currentSetting(pi, dir);
|
|
@@ -137,12 +143,17 @@ export function subagentTasks(pi: any, ctx: any, tasks: ChildTask[], options: {
|
|
|
137
143
|
delegating,
|
|
138
144
|
main,
|
|
139
145
|
explicit: task.model ?? null,
|
|
146
|
+
userPrompt: options.userPrompt ?? null,
|
|
140
147
|
refused: (model) => Boolean(isRefused(dir, model, options.now)),
|
|
141
148
|
});
|
|
142
149
|
const next: ChildTask = { ...task, env: { ...(task.env ?? {}), ...handDown } };
|
|
150
|
+
if (task.model) next.requestedModel = task.model;
|
|
143
151
|
if (resolution.model) next.model = resolution.model;
|
|
144
152
|
else delete next.model;
|
|
145
153
|
if (resolution.model && resolution.effort) next.effort = resolution.effort;
|
|
154
|
+
else delete next.effort;
|
|
155
|
+
if (resolution.source) next.modelSource = resolution.source;
|
|
156
|
+
if (resolution.override) next.override = { ...resolution.override, note: overrideNote(resolution.override, catalog) };
|
|
146
157
|
if (resolution.fallback) next.fallback = resolution.fallback;
|
|
147
158
|
if (resolution.gatewayFallback) next.gatewayFallback = resolution.gatewayFallback;
|
|
148
159
|
return next;
|
|
@@ -292,7 +303,7 @@ export default function (pi: any) {
|
|
|
292
303
|
if (patch.model !== undefined) override.model = next.model;
|
|
293
304
|
if (patch.effort !== undefined || next.effort !== current.effort) override.effort = next.effort;
|
|
294
305
|
sessionOverrides.set(pi, override);
|
|
295
|
-
if (patch.model !== undefined && current.effort && next.effort === null) {
|
|
306
|
+
if (command !== "reset" && patch.model !== undefined && current.effort && next.effort === null) {
|
|
296
307
|
notify(`Sub-agent effort ${current.effort} is not offered by ${next.model ? displayName(catalog, next.model) : "the main model"}: back to same as main.`, "warning");
|
|
297
308
|
}
|
|
298
309
|
if (next.effort && next.model === null && main.model) {
|
package/package.json
CHANGED
package/scripts/brand-engine.js
CHANGED
|
@@ -23,10 +23,16 @@
|
|
|
23
23
|
// folders), OMNIRUSH_* copies of the env the core exports to shell
|
|
24
24
|
// commands, AI_AGENT, the HTTP User-Agent and temp-file names.
|
|
25
25
|
//
|
|
26
|
+
// 4. <package>/engine, a link to the core (src/compat.js
|
|
27
|
+
// ensureEngineLink): the launcher runs the core through it, so process
|
|
28
|
+
// listings and the doc paths in the system prompt name omnirush, not
|
|
29
|
+
// the core's npm package.
|
|
30
|
+
//
|
|
26
31
|
// Not renamed (internal, never shown): package ids and imports, code
|
|
27
32
|
// identifiers, session entry/customType ids, telemetry attribute keys,
|
|
28
33
|
// protocol values third-party providers expect, and the PI_* env names the
|
|
29
|
-
// core reads (the launcher maps OMNIRUSH_* spellings onto them
|
|
34
|
+
// core reads (the launcher maps OMNIRUSH_* spellings onto them; shells the
|
|
35
|
+
// core starts get the OMNIRUSH_* copies only).
|
|
30
36
|
//
|
|
31
37
|
// Maintenance: after bumping the core, run `node scripts/brand-engine.js`
|
|
32
38
|
// and `node --test test/branding.test.js`; a target that moved is reported
|
|
@@ -38,11 +44,16 @@ import fs from "node:fs";
|
|
|
38
44
|
import path from "node:path";
|
|
39
45
|
import { fileURLToPath } from "node:url";
|
|
40
46
|
|
|
47
|
+
import { CORE_ONLY_ENV_NAMES, ensureEngineLink } from "../src/compat.js";
|
|
48
|
+
|
|
41
49
|
const PKG = "@earendil-works/pi-coding-agent";
|
|
42
50
|
const BRAND = "omnirush";
|
|
43
51
|
export const BRAND_CONFIG_DIR = ".omnirush";
|
|
44
52
|
const OUR_CHANGELOG = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "CHANGELOG.md");
|
|
45
53
|
|
|
54
|
+
/** Matches the core's own PI_<NAME> variables (src/compat.js CORE_ONLY_ENV_NAMES), as JS regex source. */
|
|
55
|
+
export const CORE_ENV_PATTERN = `/^PI_(${CORE_ONLY_ENV_NAMES.join("|")})$/`;
|
|
56
|
+
|
|
46
57
|
// `from` must match the installed dist exactly; applied with replaceAll to
|
|
47
58
|
// every dist/**/*.js. A `to` must never contain its own `from` (idempotency).
|
|
48
59
|
export const REPLACEMENTS = [
|
|
@@ -157,13 +168,37 @@ export const REPLACEMENTS = [
|
|
|
157
168
|
{
|
|
158
169
|
label: "/bug disclaimer",
|
|
159
170
|
from: "This report goes to the Pi developers (Earendil) and is not shared publicly. It includes your pi version,",
|
|
160
|
-
to: "This report
|
|
171
|
+
to: "This report is not shared publicly. It includes your omnirush version,",
|
|
161
172
|
},
|
|
162
173
|
{
|
|
163
174
|
label: "announcement easter egg",
|
|
164
175
|
from: '"pi has joined Earendil"',
|
|
165
|
-
to: '"
|
|
176
|
+
to: '"omnirush"',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
// Upstream easter eggs typed as commands (/arminsayshi, /dementedelves;
|
|
180
|
+
// the third only fires for one third-party model): shown nothing.
|
|
181
|
+
label: "hidden /arminsayshi is inert (bundle)",
|
|
182
|
+
from: "handleArminSaysHi(){",
|
|
183
|
+
to: "handleArminSaysHi(_off){return;",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
label: "hidden /dementedelves is inert (bundle)",
|
|
187
|
+
from: "handleDementedDelves(){",
|
|
188
|
+
to: "handleDementedDelves(_off){return;",
|
|
166
189
|
},
|
|
190
|
+
{ label: "upstream easter egg is inert (bundle)", from: "handleDaxnuts(){", to: "handleDaxnuts(_off){return;" },
|
|
191
|
+
{
|
|
192
|
+
label: "hidden /arminsayshi is inert (dist)",
|
|
193
|
+
from: " handleArminSaysHi() {",
|
|
194
|
+
to: " handleArminSaysHi(_off) { return;",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: "hidden /dementedelves is inert (dist)",
|
|
198
|
+
from: " handleDementedDelves() {",
|
|
199
|
+
to: " handleDementedDelves(_off) { return;",
|
|
200
|
+
},
|
|
201
|
+
{ label: "upstream easter egg is inert (dist)", from: " handleDaxnuts() {", to: " handleDaxnuts(_off) { return;" },
|
|
167
202
|
{
|
|
168
203
|
label: "external editor note",
|
|
169
204
|
from: "Pi will resume when the editor exits.",
|
|
@@ -345,7 +380,20 @@ export const REPLACEMENTS = [
|
|
|
345
380
|
label: "OMNIRUSH_* session env for shell commands",
|
|
346
381
|
from: "ctx.thinkingLevel&&(env2.PI_REASONING_LEVEL=ctx.thinkingLevel)}let baseContext={command,cwd,env:env2}",
|
|
347
382
|
to:
|
|
348
|
-
'ctx.thinkingLevel&&(env2.PI_REASONING_LEVEL=ctx.thinkingLevel)}for(let k of["SESSION_ID","SESSION_FILE","PROVIDER","MODEL","REASONING_LEVEL"])env2["PI_"+k]===void 0?delete env2["OMNIRUSH_"+k]:env2["OMNIRUSH_"+k]=env2["PI_"+k];let baseContext={command,cwd,env:env2}',
|
|
383
|
+
'ctx.thinkingLevel&&(env2.PI_REASONING_LEVEL=ctx.thinkingLevel)}for(let k of["SESSION_ID","SESSION_FILE","PROVIDER","MODEL","REASONING_LEVEL"])env2["PI_"+k]===void 0?delete env2["OMNIRUSH_"+k]:(env2["OMNIRUSH_"+k]=env2["PI_"+k],delete env2["PI_"+k]);let baseContext={command,cwd,env:env2}',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
// The env every shell the core starts inherits (the bash tool, `!`
|
|
387
|
+
// commands, package installs): the core's own PI_* variables stay in
|
|
388
|
+
// the agent process; their OMNIRUSH_* copies are what commands see.
|
|
389
|
+
label: "no core PI_* variables in shell commands (bundle)",
|
|
390
|
+
from: "return{...process.env,[pathKey]:updatedPath}}function sanitizeBinaryOutput",
|
|
391
|
+
to: `let _env={...process.env,[pathKey]:updatedPath};for(let k of Object.keys(_env))${CORE_ENV_PATTERN}.test(k)&&delete _env[k];return _env}function sanitizeBinaryOutput`,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
label: "no core PI_* variables in shell commands (dist)",
|
|
395
|
+
from: " return {\n ...process.env,\n [pathKey]: updatedPath,\n };\n}",
|
|
396
|
+
to: ` const _env = { ...process.env, [pathKey]: updatedPath };\n for (const k of Object.keys(_env)) if (${CORE_ENV_PATTERN}.test(k)) delete _env[k];\n return _env;\n}`,
|
|
349
397
|
},
|
|
350
398
|
|
|
351
399
|
// --- HTTP User-Agent (the omnirush gateway sees it) --------------------
|
|
@@ -377,6 +425,9 @@ export const REPLACEMENTS = [
|
|
|
377
425
|
{ label: "bug report zip", from: "`pi-bug-report-${id}.zip`", to: "`omnirush-bug-report-${id}.zip`" },
|
|
378
426
|
{ label: "TUI crash log", from: '"pi-tui-crash.log"', to: '"omnirush-tui-crash.log"' },
|
|
379
427
|
{ label: "TUI debug log", from: '"pi-tui-debug.log"', to: '"omnirush-tui-debug.log"' },
|
|
428
|
+
{ label: "git package update marker", from: ".pi-update-incomplete", to: ".omnirush-update-incomplete" },
|
|
429
|
+
{ label: "native module quarantine dir", from: '".pi-native-quarantine"', to: '".omnirush-native-quarantine"' },
|
|
430
|
+
{ label: "package install root package.json name", from: '"pi-extensions"', to: '"omnirush-extensions"' },
|
|
380
431
|
|
|
381
432
|
// --- update banner (belt-and-braces; the launcher sets PI_OFFLINE) -----
|
|
382
433
|
// showNewVersionNotification returns before its body (the parameter is
|
|
@@ -507,6 +558,9 @@ function main() {
|
|
|
507
558
|
console.log("omnirush: agent core not installed; skipped branding patch");
|
|
508
559
|
return;
|
|
509
560
|
}
|
|
561
|
+
// <package>/engine -> the core (what process listings and the system
|
|
562
|
+
// prompt's doc paths show; the launcher re-creates it when missing).
|
|
563
|
+
ensureEngineLink(repoRoot, report.piRoot);
|
|
510
564
|
const patched = report.packageJsonPatched ? "package.json + " : "";
|
|
511
565
|
const notes = report.changelogReplaced ? " + release notes" : "";
|
|
512
566
|
console.log(
|
package/src/bin.js
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
yoloBanner,
|
|
55
55
|
} from "./lib.js";
|
|
56
56
|
import { installManagedTools } from "./tools.js";
|
|
57
|
-
import { engineEnvAliases, migrateAgentDir, projectConfigDirName, resolveAgentDir } from "./compat.js";
|
|
57
|
+
import { engineEnvAliases, ensureEngineLink, migrateAgentDir, projectConfigDirName, resolveAgentDir } from "./compat.js";
|
|
58
58
|
import { customSessionDir, moveLegacySubagentSessions } from "./sessions.js";
|
|
59
59
|
import { ensureRuntime, managedBunPath } from "../scripts/postinstall.js";
|
|
60
60
|
import {
|
|
@@ -144,12 +144,31 @@ function piPackage() {
|
|
|
144
144
|
return null;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// The core as users see it (process listings, the doc paths in the system
|
|
148
|
+
// prompt, sub-agent command lines): <package>/engine, a link to the core's
|
|
149
|
+
// folder (src/compat.js ensureEngineLink); null when the link cannot be made.
|
|
150
|
+
let ENGINE_LINK_DIR = null;
|
|
151
|
+
let ENGINE_ENTRY = null;
|
|
152
|
+
|
|
147
153
|
function piEntry() {
|
|
148
154
|
const found = piPackage();
|
|
149
|
-
if (found?.pkg?.bin?.pi)
|
|
155
|
+
if (found?.pkg?.bin?.pi) {
|
|
156
|
+
const dir = ensureEngineLink(path.resolve(__dirname, ".."), found.dir);
|
|
157
|
+
ENGINE_LINK_DIR = dir === path.resolve(found.dir) ? null : dir;
|
|
158
|
+
ENGINE_ENTRY = path.join(dir, found.pkg.bin.pi);
|
|
159
|
+
return ENGINE_ENTRY;
|
|
160
|
+
}
|
|
150
161
|
throw new Error("could not locate the pinned agent core (broken install). Fix: npm install -g omnirush@latest");
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
/** The core's package dir as the link, unless the user points it elsewhere. */
|
|
165
|
+
function engineDirEnv(env = process.env) {
|
|
166
|
+
const env2 = {};
|
|
167
|
+
if (ENGINE_ENTRY) env2.OMNIRUSH_CORE_ENTRY = ENGINE_ENTRY; // sub-agents start the core through the same path
|
|
168
|
+
if (ENGINE_LINK_DIR && !env.PI_PACKAGE_DIR && !env.OMNIRUSH_PACKAGE_DIR) env2.PI_PACKAGE_DIR = ENGINE_LINK_DIR;
|
|
169
|
+
return env2;
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
function ensureDirs() {
|
|
154
173
|
fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
|
|
155
174
|
migrateLegacyAgentData();
|
|
@@ -875,6 +894,7 @@ function childEnv(auth) {
|
|
|
875
894
|
const origin = (process.env.OMNIRUSH_ORIGIN || "").trim().replace(/\/+$/, "") ||
|
|
876
895
|
gateway.replace(/\/+$/, "").replace(/\/v1$/, ""); // provider base -> manager origin
|
|
877
896
|
return {
|
|
897
|
+
...engineDirEnv(process.env),
|
|
878
898
|
// OMNIRUSH_* spellings of the core's own settings (PI_* still work).
|
|
879
899
|
...engineEnvAliases(process.env),
|
|
880
900
|
// Bootstrap values only: the agent (and every sub-agent it spawns)
|
|
@@ -1090,8 +1110,7 @@ Environment:
|
|
|
1090
1110
|
(default 50% of RAM), and none start below this
|
|
1091
1111
|
(default 10%); e.g. 25% or 6G. OMNIRUSH_AGENT_MEMORY=off
|
|
1092
1112
|
starts them without checking memory
|
|
1093
|
-
OMNIRUSH_<SETTING> the agent core's own settings (e.g. OMNIRUSH_CACHE_RETENTION
|
|
1094
|
-
the older PI_<SETTING> names still work)
|
|
1113
|
+
OMNIRUSH_<SETTING> the agent core's own settings (e.g. OMNIRUSH_CACHE_RETENTION)
|
|
1095
1114
|
|
|
1096
1115
|
Docs: https://omnirush.ai/omnirush — state is stored 0600 in ~/.omnirush`);
|
|
1097
1116
|
}
|
package/src/compat.js
CHANGED
|
@@ -270,3 +270,68 @@ export function engineEnvAliases(env = process.env) {
|
|
|
270
270
|
}
|
|
271
271
|
return out;
|
|
272
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The core's own variables the launcher and the core set for themselves
|
|
276
|
+
* (PI_<NAME>). Shell commands the agent runs, background jobs and MCP
|
|
277
|
+
* servers see their OMNIRUSH_<NAME> copies only; a PI_ variable that is
|
|
278
|
+
* not on this list (a user's own) passes through. Mirrored in
|
|
279
|
+
* scripts/brand-engine.js (the core's shell env) and
|
|
280
|
+
* assets/extensions/omnirush/secret-env.ts (tests keep them equal).
|
|
281
|
+
*/
|
|
282
|
+
export const CORE_ONLY_ENV_NAMES = [
|
|
283
|
+
"CODING_AGENT",
|
|
284
|
+
"CODING_AGENT_DIR",
|
|
285
|
+
"CODING_AGENT_SESSION_DIR",
|
|
286
|
+
"SESSION_ID",
|
|
287
|
+
"SESSION_FILE",
|
|
288
|
+
"PROVIDER",
|
|
289
|
+
"MODEL",
|
|
290
|
+
"REASONING_LEVEL",
|
|
291
|
+
"OFFLINE",
|
|
292
|
+
"TELEMETRY",
|
|
293
|
+
"INSTALLER_API_BASE",
|
|
294
|
+
"MANAGED_INSTALL_ROOT",
|
|
295
|
+
...ENGINE_ENV_NAMES,
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
/** Folder, in the omnirush package, that links to the installed agent core. */
|
|
299
|
+
export const ENGINE_LINK = "engine";
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The agent core as users see it: <package root>/engine, a link to the
|
|
303
|
+
* core's folder in node_modules (a junction on Windows). The launcher runs
|
|
304
|
+
* the core through it and hands it over as the core's package dir, so
|
|
305
|
+
* process listings, the doc paths in the system prompt and the sub-agent
|
|
306
|
+
* command lines name omnirush instead of the core's npm package. Made by
|
|
307
|
+
* the branding step at install and, when missing (archives, a read-only
|
|
308
|
+
* reinstall), here at launch. Never replaces a real folder. Returns the
|
|
309
|
+
* link, or the core's own folder when the link cannot be made.
|
|
310
|
+
*/
|
|
311
|
+
export function ensureEngineLink(packageRoot, coreDir, { fsImpl = fs, platform = process.platform } = {}) {
|
|
312
|
+
const link = path.join(packageRoot, ENGINE_LINK);
|
|
313
|
+
const real = path.resolve(coreDir);
|
|
314
|
+
const points = () => {
|
|
315
|
+
try {
|
|
316
|
+
return fsImpl.realpathSync(link) === fsImpl.realpathSync(real);
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
if (points()) return link;
|
|
322
|
+
try {
|
|
323
|
+
let stat = null;
|
|
324
|
+
try {
|
|
325
|
+
stat = fsImpl.lstatSync(link);
|
|
326
|
+
} catch {
|
|
327
|
+
/* absent */
|
|
328
|
+
}
|
|
329
|
+
if (stat && !stat.isSymbolicLink()) return real;
|
|
330
|
+
if (stat) fsImpl.unlinkSync(link); // a stale link (the core moved)
|
|
331
|
+
if (platform === "win32") fsImpl.symlinkSync(real, link, "junction");
|
|
332
|
+
else fsImpl.symlinkSync(path.relative(path.dirname(link), real) || ".", link, "dir");
|
|
333
|
+
} catch {
|
|
334
|
+
/* read-only install or a concurrent launch: checked below */
|
|
335
|
+
}
|
|
336
|
+
return points() ? link : real;
|
|
337
|
+
}
|