omnirush 0.10.2 → 0.10.3
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 +48 -5
- package/assets/extensions/omnirush/agents.ts +78 -14
- 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
|
@@ -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";
|
|
@@ -305,8 +306,8 @@ export interface ChildTask {
|
|
|
305
306
|
role: AgentRole;
|
|
306
307
|
task: string;
|
|
307
308
|
/** Cross-model children: gateway model id the child runs on
|
|
308
|
-
* (e.g. "muse-spark-1.
|
|
309
|
-
*
|
|
309
|
+
* (e.g. "muse-spark-1.1" workers under an astra parent). Undefined =
|
|
310
|
+
* inherit the parent's model. */
|
|
310
311
|
model?: string;
|
|
311
312
|
/** The effort the child runs on (gateway spelling); undefined = pi's default. */
|
|
312
313
|
effort?: string;
|
|
@@ -316,6 +317,21 @@ export interface ChildTask {
|
|
|
316
317
|
gatewayFallback?: { model: string; effort: string | null };
|
|
317
318
|
/** Extra environment for the child (the sub-agent setting and main model, for nested layers). */
|
|
318
319
|
env?: Record<string, string>;
|
|
320
|
+
/** The model[:effort] the delegating agent asked for (the task's own `model`), when it named one. */
|
|
321
|
+
requestedModel?: string;
|
|
322
|
+
/** That model did not run: the user's /subagents choice won, or the gateway does not serve it. */
|
|
323
|
+
override?: ChildModelOverride;
|
|
324
|
+
/** Where `model` comes from ("subagents", "user_prompt", "task", "parent"). */
|
|
325
|
+
modelSource?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A task model that did not run, and what ran instead (the result's model_override). */
|
|
329
|
+
export interface ChildModelOverride {
|
|
330
|
+
requested: string;
|
|
331
|
+
used: string;
|
|
332
|
+
effort: string | null;
|
|
333
|
+
reason: string;
|
|
334
|
+
note: string;
|
|
319
335
|
}
|
|
320
336
|
|
|
321
337
|
/** A sub-agent that ran on the main model instead of the picked one. */
|
|
@@ -342,6 +358,12 @@ export interface ChildResult {
|
|
|
342
358
|
effort?: string;
|
|
343
359
|
/** It ran on the main model instead of the picked one (why, and since when). */
|
|
344
360
|
model_fallback?: ChildModelFallback;
|
|
361
|
+
/** The task's own model did not run (the user's /subagents choice, or not served). */
|
|
362
|
+
model_override?: ChildModelOverride;
|
|
363
|
+
/** Where the model comes from: "subagents", "user_prompt", "task" or "parent". */
|
|
364
|
+
model_source?: string;
|
|
365
|
+
/** The model[:effort] the task asked for, when it named one. */
|
|
366
|
+
requested_model?: string;
|
|
345
367
|
status: ChildStatus;
|
|
346
368
|
/** Process exit code (null when killed by a signal or still unknown). */
|
|
347
369
|
exitCode: number | null;
|
|
@@ -412,6 +434,9 @@ export function buildChildResult(
|
|
|
412
434
|
...(task.model ? { model: task.model } : {}),
|
|
413
435
|
...(task.effort ? { effort: task.effort } : {}),
|
|
414
436
|
...(modelFallback ? { model_fallback: modelFallback } : {}),
|
|
437
|
+
...(task.override ? { model_override: { ...task.override } } : {}),
|
|
438
|
+
...(task.modelSource ? { model_source: task.modelSource } : {}),
|
|
439
|
+
...(task.requestedModel ? { requested_model: task.requestedModel } : {}),
|
|
415
440
|
status: input.status ?? (input.exitCode === 0 ? "completed" : "failed"),
|
|
416
441
|
exitCode: input.exitCode,
|
|
417
442
|
output: capped,
|
|
@@ -921,16 +946,34 @@ export async function mapWithConcurrency<TIn, TOut>(
|
|
|
921
946
|
return results;
|
|
922
947
|
}
|
|
923
948
|
|
|
949
|
+
/** The model and effort a child really ran on (after a gateway fallback), and why that one. */
|
|
950
|
+
export function ranOn(result: Pick<ChildResult, "model" | "effort" | "model_fallback">): { model: string | null; effort: string | null } {
|
|
951
|
+
if (result.model_fallback?.kind === "gateway") return { model: result.model_fallback.used, effort: result.model_fallback.effort ?? null };
|
|
952
|
+
return { model: result.model ?? null, effort: result.effort ?? null };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function ranOnText(result: ChildResult): string {
|
|
956
|
+
const { model, effort } = ranOn(result);
|
|
957
|
+
if (!model) return "the parent's model (not an omnirush.ai model)";
|
|
958
|
+
const why = result.model_fallback?.kind === "gateway"
|
|
959
|
+
? "the gateway refused the chosen model"
|
|
960
|
+
: result.model_fallback
|
|
961
|
+
? "the main model"
|
|
962
|
+
: sourceNote(result.model_source);
|
|
963
|
+
return `${model}${effort ? ` · ${effort}` : ""}${why ? ` (${why})` : ""}`;
|
|
964
|
+
}
|
|
965
|
+
|
|
924
966
|
/** Structured result text for the parent model (one section per child). */
|
|
925
967
|
export function renderChildResults(results: ChildResult[], ids?: readonly string[]): string {
|
|
926
968
|
const succeeded = results.filter((result) => result.status === "completed").length;
|
|
927
969
|
const sections = results.map((result, index) => {
|
|
928
970
|
const minutes = Math.round((result.durationMs / 60_000) * 10) / 10;
|
|
929
|
-
const
|
|
930
|
-
const
|
|
931
|
-
const label = ranOn ? ` [${ranOn}${effort ? ` · ${effort}` : ""}]` : "";
|
|
971
|
+
const ran = ranOn(result);
|
|
972
|
+
const label = ran.model ? ` [${ran.model}${ran.effort ? ` · ${ran.effort}` : ""}]` : "";
|
|
932
973
|
const header = `### ${ids?.[index] ? `${ids[index]} ` : ""}${result.role}${label} — ${result.status} (${minutes} min${result.outputTruncated ? ", output capped" : ""})`;
|
|
933
974
|
const meta: string[] = [`task: ${result.task}`];
|
|
975
|
+
meta.push(`ran on: ${ranOnText(result)}`);
|
|
976
|
+
if (result.model_override) meta.push(`note: ${result.model_override.note}`);
|
|
934
977
|
if (result.model_fallback) meta.push(`note: ${result.model_fallback.note}`);
|
|
935
978
|
if (result.error) meta.push(`error: ${result.error}`);
|
|
936
979
|
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
|
};
|
|
@@ -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) {
|