@irythill/orq 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,7 +135,7 @@ roles = ["impl", "fix", "review"]
135
135
  quota = ["codex"] # name in agent-quota's output
136
136
  [agents.codex.models.impl]
137
137
  simple = "gpt-6-sol@low" # model@effort, per issue level
138
- medium = "gpt-6-sol@medium"
138
+ medium = ["gpt-6-sol@medium", "gpt-6-luna@medium"] # several: each (agent, model) pair is scored on its own history
139
139
  heavy = "gpt-6.1-sol@high"
140
140
  [agents.codex.models.review]
141
141
  simple = "gpt-6-sol@low" # reviews scale with the issue too
package/README.pt-BR.md CHANGED
@@ -138,7 +138,7 @@ roles = ["impl", "fix", "review"]
138
138
  quota = ["codex"] # name in agent-quota's output
139
139
  [agents.codex.models.impl]
140
140
  simple = "gpt-6-sol@low" # model@effort, per issue level
141
- medium = "gpt-6-sol@medium"
141
+ medium = ["gpt-6-sol@medium", "gpt-6-luna@medium"] # several: each (agent, model) pair is scored on its own history
142
142
  heavy = "gpt-6.1-sol@high"
143
143
  [agents.codex.models.review]
144
144
  simple = "gpt-6-sol@low" # reviews scale with the issue too
package/dist/pick.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Picks agent, model, effort and account for a role (port of orq-pick, plus fixed roles).
2
2
  // fixed role: the first agent of [agent, ...fallback] that passes the filters wins — no scoring, never swapped
3
3
  // for an agent the user didn't list.
4
- // auto role: every pool agent that passes is scored:
4
+ // auto role: every (agent, model) pair that passes is scored (a level may list several models):
5
5
  // quality * delivery + tokens * economy + speed * quickness − rotation * (picks in the last `window`)
6
6
  // − reserve (agents with reserve = true) + explore (fewer than `min_samples` runs, fading with n, none after
7
7
  // `explore_streak` picks in a row). Each axis is (prior·k + observed·n)/(k + n), k = min_samples.
@@ -73,17 +73,25 @@ export function limitsFor(pol, level) {
73
73
  const five = { ...FIVE, ...(pol.quota_5h ?? {}) };
74
74
  return [Math.min(Number(five[level] ?? five.medium), Number(pol.quota_limit ?? 90)), Number(pol.quota_week ?? 95)];
75
75
  }
76
- export function modelFor(a, role, level) {
77
- if (a.models === undefined)
78
- return ["", undefined]; // no models configured: the CLI's default
79
- const models = a.models;
76
+ /** The agent's models section for a role, with the same fallbacks as the roles (fix → impl, watchdog → opinion → review). */
77
+ function roleModels(a, role) {
78
+ const models = (a.models ?? {});
80
79
  const lr = legacyRole(role);
81
- const ms = models[lr] ?? (lr === "fix" ? models.impl : lr === "watchdog" ? models.opinion ?? models.review : undefined) ?? {};
82
- const spec = ms[level] ?? ms.default ?? ms.medium ?? ms.simple;
83
- if (!spec)
84
- return undefined;
85
- const [model, effort] = spec.split("@");
86
- return [model, effort || undefined];
80
+ return models[lr] ?? (lr === "fix" ? models.impl : lr === "watchdog" ? models.opinion ?? models.review : undefined) ?? {};
81
+ }
82
+ const specList = (spec) => (Array.isArray(spec) ? spec : spec ? [spec] : []).map(String).filter(Boolean)
83
+ .map((x) => { const [model, effort] = x.split("@"); return [model, effort || undefined]; });
84
+ /** Every candidate model for the role/level, in the order configured; undefined when none is. */
85
+ export function modelsFor(a, role, level) {
86
+ if (a.models === undefined)
87
+ return [["", undefined]]; // no models configured: the CLI's default
88
+ const ms = roleModels(a, role);
89
+ const list = specList(ms[level] ?? ms.default ?? ms.medium ?? ms.simple);
90
+ return list.length ? list : undefined;
91
+ }
92
+ /** The first candidate model for the role/level. */
93
+ export function modelFor(a, role, level) {
94
+ return modelsFor(a, role, level)?.[0];
87
95
  }
88
96
  // --- per-model quota ---------------------------------------------------------------------------------------------------
89
97
  // A collector may add lines "<account>/<model> 5h=.. week=.." (antigravity: one bucket per model group). The model a pick is about
@@ -98,20 +106,19 @@ export function modelKey(model, keys) {
98
106
  }
99
107
  /** The models of an agent's role section lighter than `level`, the closest first, without repeats of `tried`. */
100
108
  export function lighterModels(a, role, level, tried) {
101
- const models = a.models;
102
- if (!models)
109
+ if (!a.models)
103
110
  return [];
104
- const lr = legacyRole(role);
105
- const ms = models[lr] ?? (lr === "fix" ? models.impl : lr === "watchdog" ? models.opinion ?? models.review : undefined) ?? {};
111
+ const ms = roleModels(a, role);
106
112
  const rank = LEVEL_RANK[level] ?? 1, seen = new Set(tried);
107
113
  const out = [];
108
114
  for (const [, spec] of Object.entries(ms).filter(([lv]) => (LEVEL_RANK[lv] ?? 1) < rank)
109
115
  .sort((x, y) => (LEVEL_RANK[y[0]] ?? 1) - (LEVEL_RANK[x[0]] ?? 1))) {
110
- const [model, effort] = spec.split("@");
111
- if (seen.has(model))
112
- continue;
113
- seen.add(model);
114
- out.push([model, effort || undefined]);
116
+ for (const [model, effort] of specList(spec)) {
117
+ if (seen.has(model))
118
+ continue;
119
+ seen.add(model);
120
+ out.push([model, effort]);
121
+ }
115
122
  }
116
123
  return out;
117
124
  }
@@ -187,8 +194,8 @@ export function pick(inp) {
187
194
  out(name, m("ignores prompts (a review tool)", "ignora prompts (ferramenta de revisão)"));
188
195
  continue;
189
196
  }
190
- const mf = modelFor(a, role, level);
191
- if (!mf) {
197
+ const mfs = modelsFor(a, role, level);
198
+ if (!mfs) {
192
199
  out(name, m(`no model for ${role}/${level}`, `sem modelo para ${role}/${level}`));
193
200
  continue;
194
201
  }
@@ -247,8 +254,8 @@ export function pick(inp) {
247
254
  return { pace: paces.length ? Math.max(...paces) : undefined,
248
255
  hot: penalty > 0 && ((p5 !== undefined && p5 > limit5) || (pw !== undefined && pw > limitWeek)) };
249
256
  };
250
- // The chosen account's per-model buckets: an exhausted model gives way to a lighter one of the same agent.
251
- let mfx = mf;
257
+ // The chosen account's per-model buckets: an exhausted candidate drops out; with none left, a lighter model of the agent.
258
+ let mfx = mfs;
252
259
  const acc0 = usable.length ? [...usable].sort((x, y) => x[1] - y[1])[0][0] : undefined;
253
260
  const mkeys = acc0 ? Object.keys(inp.windows ?? {}).filter((k) => k.startsWith(`${acc0}/`)).map((k) => k.slice(acc0.length + 1)) : [];
254
261
  if (acc0 && mkeys.length) {
@@ -263,23 +270,25 @@ export function pick(inp) {
263
270
  return `${model} ${five ? `5h ${q.five}%` : `${m("week", "semana")} ${q.week}%`}`
264
271
  + (at.length ? ` (${m("back", "volta")} ${humanReset(Math.max(...at))})` : "");
265
272
  };
266
- const tried = [], skipped = [];
267
- let found;
268
- for (const cand of [mf, ...lighterModels(a, role, level, [mf[0]])]) {
269
- const why = over(cand[0]);
270
- if (!why) {
271
- found = cand;
272
- break;
273
+ const skipped = [];
274
+ let found = mfs.filter((c) => { const why = over(c[0]); if (why)
275
+ skipped.push(why); return !why; });
276
+ if (!found.length) {
277
+ for (const cand of lighterModels(a, role, level, mfs.map((c) => c[0]))) {
278
+ const why = over(cand[0]);
279
+ if (!why) {
280
+ found = [cand];
281
+ break;
282
+ }
283
+ skipped.push(why);
273
284
  }
274
- tried.push(cand[0]);
275
- skipped.push(why);
276
285
  }
277
- if (!found) {
286
+ if (!found.length) {
278
287
  out(name, `${m("every model over its quota", "todo modelo acima da cota")}: ${skipped.join(", ")}`);
279
288
  continue;
280
289
  }
281
290
  if (skipped.length)
282
- out(name, `${m("quota exhausted", "cota esgotada")}: ${skipped.join(", ")}; ${m("using", "usando")} ${found[0]}`);
291
+ out(name, `${m("quota exhausted", "cota esgotada")}: ${skipped.join(", ")}; ${m("using", "usando")} ${found.map((c) => c[0]).join(", ")}`);
283
292
  mfx = found;
284
293
  }
285
294
  const ranked = usable.map(([n, v]) => ({ n, v: v, ...paceOf(n) }))
@@ -287,9 +296,12 @@ export function pick(inp) {
287
296
  const chosen = ranked[0];
288
297
  const acct = qn.length > 1 ? chosen?.n : undefined;
289
298
  const statRole = role === "fix" || role === "worker" ? "impl" : role;
290
- const stOf = (sr) => inp.stats.find((r) => r.role === sr && r.agent === name && r.model === (mfx[0].split("/").pop() || "default"));
291
- const st = stOf(statRole) ?? (role === "watchdog" ? stOf("review") : undefined); // no watchdog runs yet: its review record
292
- pass.push({ agent: name, model: mfx[0], effort: mfx[1], acct, a, st, pace: chosen?.pace, hot: Boolean(chosen?.hot) });
299
+ // One candidate per (agent, model) pair, each scored on its own stats row; a fixed role takes the first model.
300
+ for (const [model, effort] of res.mode === "fixed" ? mfx.slice(0, 1) : mfx) {
301
+ const stOf = (sr) => inp.stats.find((r) => r.role === sr && r.agent === name && r.model === (model.split("/").pop() || "default"));
302
+ const st = stOf(statRole) ?? (role === "watchdog" ? stOf("review") : undefined); // no watchdog runs yet: its review record
303
+ pass.push({ agent: name, model, effort, acct, a, st, pace: chosen?.pace, hot: Boolean(chosen?.hot) });
304
+ }
293
305
  if (res.mode === "fixed")
294
306
  break; // the first that passes wins
295
307
  }
@@ -324,9 +336,10 @@ export function pick(inp) {
324
336
  const t = axis(m("economy", "economia"), prior, c.st?.new && refTok ? Math.min(1, refTok / c.st.new) : undefined, n);
325
337
  const s = axis(m("speed", "rapidez"), prior, c.st?.min && refMin ? Math.min(1, refMin / Math.max(c.st.min, 0.1)) : undefined, n);
326
338
  const used = inp.recent.filter((p) => p.agent === c.agent).length;
327
- // Exploration fades as samples come in and stops after explore_streak picks of the agent in a row.
339
+ // Exploration fades as samples come in and stops after explore_streak picks of the pair in a row (rotation stays per agent).
340
+ const same = (p) => p.agent === c.agent && (p.model === undefined || p.model === c.model);
328
341
  let streak = 0;
329
- for (let i = inp.recent.length - 1; i >= 0 && inp.recent[i].agent === c.agent; i--)
342
+ for (let i = inp.recent.length - 1; i >= 0 && same(inp.recent[i]); i--)
330
343
  streak++;
331
344
  const capped = few(c.st) && streak >= w.explore_streak;
332
345
  const explore = few(c.st) && !capped ? w.explore * (k - n) / k : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irythill/orq",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Orchestrate coding agents (Claude Code, Codex, OpenCode, Antigravity, Copilot) from one conductor.",
5
5
  "license": "MIT",
6
6
  "type": "module",