@polderlabs/bizar 10.23.10 → 10.23.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.23.10",
3
+ "version": "10.23.11",
4
4
  "description": "Autonomous, human-in-the-loop multi-agent harness for Claude Code with guarded workflows, typed SDK primitives, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -225,8 +225,13 @@ export declare function rankUserSelectedForRole(registry: ModelRegistry, role: s
225
225
  };
226
226
  /**
227
227
  * Pure sorter extracted for testability. Mutates and returns the input
228
- * array. Sort key: `(eligible desc, capabilityScore desc, hasProfile desc,
229
- * originalIndex asc)`.
228
+ * array. Sort key: `(eligible desc, originalIndex asc, capabilityScore desc,
229
+ * hasProfile desc)`. The `originalIndex` primary sort key honours the
230
+ * operator's `userSelected.models` ordering as the authoritative pick
231
+ * sequence — the same contract as the CLI's `resolveDispatchModel`. The
232
+ * capability score is a secondary tiebreaker for entries that share the
233
+ * same `originalIndex` position (i.e., when a profile lookup changes
234
+ * eligibility status without reordering).
230
235
  */
231
236
  export declare function compareRankedEntries(a: RankedUserSelectedEntry, b: RankedUserSelectedEntry): number;
232
237
  /**
@@ -85,8 +85,6 @@ export function loadModelRegistry(src = {}) {
85
85
  if (!isRecord(value))
86
86
  registryError("CONFIG_INVALID", `Tier ${name} is invalid.`);
87
87
  const ids = modelIds(value.models);
88
- if (ids.length === 0)
89
- registryError("CONFIG_INVALID", `Tier ${name} has no candidate models.`);
90
88
  tiers.set(name, { tier: name, modelIds: ids, purpose: String(value.purpose || ""), effort: typeof value.effort === "string" ? value.effort : null });
91
89
  }
92
90
  const roleDefaults = new Map();
@@ -98,6 +96,25 @@ export function loadModelRegistry(src = {}) {
98
96
  const configuredEndpoint = typeof raw.endpoint === "string" ? raw.endpoint : typeof gateway.endpoint === "string" ? gateway.endpoint : null;
99
97
  const endpoint = process.env.BIZAR_MODEL_ROUTER_URL ?? process.env.ANTHROPIC_BASE_URL ?? configuredEndpoint;
100
98
  const userSelected = parseUserSelected(raw.userSelected);
99
+ // Empty tier lists are tolerated when the operator's `userSelected`
100
+ // pool is non-empty: the runtime resolver (resolveTierModel) prefers
101
+ // the operator's authoritative picks and only falls through to a
102
+ // tier's static model list when no user pick matches. The CLI mirror
103
+ // (config/agents/model-assignment.mjs#resolveDispatchModel) applies
104
+ // the same rule; this keeps SDK and CLI symmetric on the canonical
105
+ // template, which ships empty tier lists by design (F-201 follow-up:
106
+ // remove every shipped provider/model default). When `userSelected`
107
+ // is also empty, every tier must still carry at least one candidate
108
+ // so the resolver never dispatches with no model at all.
109
+ if (!(userSelected && userSelected.models.length > 0)) {
110
+ for (const [name, value] of Object.entries(raw.tiers)) {
111
+ if (!isRecord(value))
112
+ registryError("CONFIG_INVALID", `Tier ${name} is invalid.`);
113
+ const ids = modelIds(value.models);
114
+ if (ids.length === 0)
115
+ registryError("CONFIG_INVALID", `Tier ${name} has no candidate models.`);
116
+ }
117
+ }
101
118
  return { version: String(raw.version || "unknown"), endpoint, configuredEndpoint, gateway, policies: raw.policies, tiers, roleDefaults, ...(userSelected ? { userSelected } : {}) };
102
119
  }
103
120
  function parseUserSelected(raw) {
@@ -298,24 +315,35 @@ export function rankUserSelectedForRole(registry, role, requirements = {}) {
298
315
  return entry;
299
316
  });
300
317
  ranked.sort(compareRankedEntries);
301
- const eligible = ranked.filter((entry) => entry.eligible);
302
- // role is consumed only for future per-role filtering / logging hooks.
303
- void role;
318
+ // Tier-aware filtering (parity with the CLI's
319
+ // resolveDispatchModel#hinted): when the caller asks for a specific
320
+ // tier (the common dispatch path passes the agent's role-default
321
+ // tier), narrow `eligible` to entries whose tier hint matches. When
322
+ // no entry matches the requested tier, fall back to the full eligible
323
+ // ranking so a stale tierHints map never strands a dispatch with no
324
+ // candidates at all — same fail-open shape as the CLI.
325
+ const tierMatched = role ? ranked.filter((entry) => entry.eligible && entry.tier === role) : ranked.filter((entry) => entry.eligible);
326
+ const eligible = tierMatched.length > 0 ? tierMatched : ranked.filter((entry) => entry.eligible);
304
327
  return { ranked, eligible };
305
328
  }
306
329
  /**
307
330
  * Pure sorter extracted for testability. Mutates and returns the input
308
- * array. Sort key: `(eligible desc, capabilityScore desc, hasProfile desc,
309
- * originalIndex asc)`.
331
+ * array. Sort key: `(eligible desc, originalIndex asc, capabilityScore desc,
332
+ * hasProfile desc)`. The `originalIndex` primary sort key honours the
333
+ * operator's `userSelected.models` ordering as the authoritative pick
334
+ * sequence — the same contract as the CLI's `resolveDispatchModel`. The
335
+ * capability score is a secondary tiebreaker for entries that share the
336
+ * same `originalIndex` position (i.e., when a profile lookup changes
337
+ * eligibility status without reordering).
310
338
  */
311
339
  export function compareRankedEntries(a, b) {
312
340
  if (a.eligible !== b.eligible)
313
341
  return a.eligible ? -1 : 1;
342
+ if (a.originalIndex !== b.originalIndex)
343
+ return a.originalIndex - b.originalIndex;
314
344
  if (a.capabilityScore !== b.capabilityScore)
315
345
  return b.capabilityScore - a.capabilityScore;
316
- if (a.hasProfile !== b.hasProfile)
317
- return a.hasProfile ? -1 : 1;
318
- return a.originalIndex - b.originalIndex;
346
+ return a.hasProfile !== b.hasProfile ? (a.hasProfile ? -1 : 1) : 0;
319
347
  }
320
348
  /**
321
349
  * Weighted capability score: reasoning=0.3, toolCall=0.25,
@@ -175,13 +175,17 @@ export function rankUserSelectedForRole(registry, role, requirements = {}) {
175
175
  });
176
176
  ranked.sort((a, b) => {
177
177
  if (a.eligible !== b.eligible) return a.eligible ? -1 : 1;
178
+ if (a.originalIndex !== b.originalIndex) return a.originalIndex - b.originalIndex;
178
179
  if (a.capabilityScore !== b.capabilityScore) return b.capabilityScore - a.capabilityScore;
179
- if (a.hasProfile !== b.hasProfile) return a.hasProfile ? -1 : 1;
180
- return a.originalIndex - b.originalIndex;
180
+ return a.hasProfile !== b.hasProfile ? (a.hasProfile ? -1 : 1) : 0;
181
181
  });
182
- const eligible = ranked.filter((entry) => entry.eligible);
183
- // role is consumed only for future per-role filtering / logging hooks.
184
- void role;
182
+ // Tier-aware filtering (parity with the CLI's
183
+ // resolveDispatchModel#hinted): narrow `eligible` to entries whose
184
+ // tier hint matches the requested tier; fall back to the full eligible
185
+ // ranking when no tier-matched candidate exists so a stale tierHints
186
+ // map never strands a dispatch with no candidates at all.
187
+ const tierMatched = role ? ranked.filter((entry) => entry.eligible && entry.tier === role) : ranked.filter((entry) => entry.eligible);
188
+ const eligible = tierMatched.length > 0 ? tierMatched : ranked.filter((entry) => entry.eligible);
185
189
  return { ranked, eligible };
186
190
  }
187
191
 
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export declare const SDK_VERSION: "10.23.9";
4
+ export declare const SDK_VERSION: "10.23.11";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export const SDK_VERSION = "10.23.9";
4
+ export const SDK_VERSION = "10.23.11";
5
5
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-sdk",
3
- "version": "10.23.10",
3
+ "version": "10.23.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",