omp-llm-role 1.0.0 → 1.2.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 +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/src/agent-create.ts
CHANGED
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
* updater in-process right after (the same path `/refresh-roles` takes).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { AGENT_NAME_RE, RESERVED_AGENT_NAMES, isReadOnlyTools, projectAgentsDir, renderAgentFile, userAgentsDir, writeAgentFile } from "./agent-file.ts";
|
|
17
|
-
import type
|
|
16
|
+
import { AGENT_NAME_RE, RESERVED_AGENT_NAMES, isReadOnlyTools, projectAgentsDir, removeAgentFile, renderAgentFile, userAgentsDir, writeAgentFile } from "./agent-file.ts";
|
|
17
|
+
import { CAPABILITY_FILL, cardinalMetric, rankRole, type Model, type RoleDef, type SuffixLevel } from "./engine.ts";
|
|
18
|
+
import { FEATURES, featureById } from "./features.ts";
|
|
18
19
|
import { metricMeta } from "./explorer/explain.ts";
|
|
19
20
|
import { ARCHETYPES, archetypeById, fitArchetype, type Archetype } from "./role-archetypes.ts";
|
|
20
|
-
import { writeRoleSettings } from "./role-settings.ts";
|
|
21
|
+
import { writeRoleSettings, type RoleWriteResult } from "./role-settings.ts";
|
|
21
22
|
import { isKnownMetric, KNOWN_METRICS, PLUGIN_SETTINGS_PATH } from "./settings.ts";
|
|
22
|
-
import { catalogMetric, type BenchmarkCatalogEntry } from "./benchmark-sources.ts";
|
|
23
|
+
import { catalogBenchmarkDeclaration, catalogMetric, sourceForMetric, type BenchmarkCatalogEntry, type FocusSourceInfo, type SourceDeclaration } from "./benchmark-sources.ts";
|
|
23
24
|
|
|
24
25
|
/** The architect's output (omp's `/agents` hub contract): the routing rule and
|
|
25
26
|
* the system prompt, plus the identifier omp would use for the file name. */
|
|
@@ -42,6 +43,14 @@ export type CreateAgentRequest = {
|
|
|
42
43
|
tools?: string[];
|
|
43
44
|
scope: "user" | "project";
|
|
44
45
|
image?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Capability flags to author on the role (`roles.<name>.features.<id>`). The
|
|
48
|
+
* request wins over the archetype's own `features` list; an unknown id is
|
|
49
|
+
* rejected by the parser before any write. The flag is persisted, not the
|
|
50
|
+
* knobs it expands to, so a later change to the recommendation still reaches
|
|
51
|
+
* the role.
|
|
52
|
+
*/
|
|
53
|
+
features?: Record<string, boolean>;
|
|
45
54
|
/** Override the generated body. */
|
|
46
55
|
body?: string;
|
|
47
56
|
/**
|
|
@@ -66,6 +75,13 @@ export type CreateAgentRequest = {
|
|
|
66
75
|
* body's `<criteria>` (the architect path gets them in its prompt instead).
|
|
67
76
|
*/
|
|
68
77
|
benchmarkLabels?: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Per-metric coverage of the ranking field, for the focus-coverage gate and
|
|
80
|
+
* the report annotation. The extension loads the ranking universe once and
|
|
81
|
+
* supplies it; omitted (the CLI, tests) means the field size is unknown and
|
|
82
|
+
* the share rule is skipped.
|
|
83
|
+
*/
|
|
84
|
+
coverage?: FocusCoverage;
|
|
69
85
|
/** Overwrite an existing agent file. */
|
|
70
86
|
force: boolean;
|
|
71
87
|
dryRun: boolean;
|
|
@@ -148,7 +164,7 @@ function buildBody(request: CreateAgentRequest, archetype: Archetype, readOnly:
|
|
|
148
164
|
* half-enforces (it accepts Σ ∈ [0.99, 1.01]; `rankRole` computes
|
|
149
165
|
* `q = Σ (wᵢ/(1−w_price))·tᵢ`, so any other split silently rescales q against λ).
|
|
150
166
|
*/
|
|
151
|
-
function checkWeightMath(name: string, weights: Record<string, number>, required: readonly string[]): string[] {
|
|
167
|
+
export function checkWeightMath(name: string, weights: Record<string, number>, required: readonly string[]): string[] {
|
|
152
168
|
const errors: string[] = [];
|
|
153
169
|
const metrics = Object.keys(weights);
|
|
154
170
|
if (metrics.length === 0) return [`role ${name}: no weights given`];
|
|
@@ -183,6 +199,10 @@ export type BenchmarkApplication = {
|
|
|
183
199
|
added: string[];
|
|
184
200
|
duplicates: string[];
|
|
185
201
|
unknown: string[];
|
|
202
|
+
/** Focus metrics dropped by the top-K cap (priority order preserved). */
|
|
203
|
+
dropped: string[];
|
|
204
|
+
/** Nine-axis assessment per focus metric, for the report annotation. */
|
|
205
|
+
assessments: Record<string, FocusMetricAssessment>;
|
|
186
206
|
};
|
|
187
207
|
|
|
188
208
|
/** The backbone every role keeps: the general/reasoning quality axes plus the
|
|
@@ -194,6 +214,493 @@ const BACKBONE_METRICS: Record<string, true> = { general: true, reasoning: true,
|
|
|
194
214
|
const FOCUS_SHARE_FLOOR = 0.25;
|
|
195
215
|
const FOCUS_SHARE_CAP = 0.4;
|
|
196
216
|
|
|
217
|
+
/** The most focus metrics one role may weight, so one benchmark keeps a decisive
|
|
218
|
+
* share instead of five sharing the budget. */
|
|
219
|
+
export const FOCUS_METRIC_CAP = 3;
|
|
220
|
+
|
|
221
|
+
/** The coverage share a fill-0 focus metric must clear to be safe to weight. */
|
|
222
|
+
export const FOCUS_COVERAGE_FLOOR = 0.35;
|
|
223
|
+
|
|
224
|
+
/** The dispersion floor: IQR/median of the cardinal-normalized covered values.
|
|
225
|
+
* A metric flatter than this cannot separate models (saturation). */
|
|
226
|
+
export const FOCUS_DISPERSION_FLOOR = 0.05;
|
|
227
|
+
|
|
228
|
+
/** The staleness bar: months the covered set may trail the pool's newest model
|
|
229
|
+
* before the source is flagged as frozen. */
|
|
230
|
+
export const FOCUS_STALENESS_MONTHS = 1;
|
|
231
|
+
|
|
232
|
+
/** The dataset-staleness bar: months since the catalog row's `updated_at` before
|
|
233
|
+
* a benchmark's dataset is flagged as unmaintained. The issue's user story is
|
|
234
|
+
* "not updated in over a year", so 12 months. This is the **dataset-date** axis,
|
|
235
|
+
* distinct from `FOCUS_STALENESS_MONTHS` (the **model-date** freshness axis):
|
|
236
|
+
* freshness asks whether the benchmark tracks the model field, maintenance asks
|
|
237
|
+
* whether the benchmark *project* is alive. A benchmark whose dataset was last
|
|
238
|
+
* touched years ago but whose rows happen to include one recent model passes
|
|
239
|
+
* freshness and is caught here. */
|
|
240
|
+
export const FOCUS_DATASET_STALENESS_MONTHS = 12;
|
|
241
|
+
|
|
242
|
+
/** The pool share above which an org's absence from the covered set is flagged
|
|
243
|
+
* (a smaller provider's absence is not a false positive). */
|
|
244
|
+
export const FOCUS_ORG_MIN_SHARE = 0.1;
|
|
245
|
+
|
|
246
|
+
/** The self-reported share above which a source is flagged as vendor-submitted.
|
|
247
|
+
* The per-entry `self_reported` flag is the varying trust signal (measured
|
|
248
|
+
* 2026-10-07: gpqa 19/20, deepswe-1.1 14/20, terminal-bench-4.0 10/20,
|
|
249
|
+
* aa-omniscience-index 2/3, automationbench-aa 0/1, the general benchmarks
|
|
250
|
+
* ~20/20). A strict majority (0.5) flags the mostly-self-reported general/gpqa
|
|
251
|
+
* sources while passing an independently measured one (automationbench 0/1) and
|
|
252
|
+
* a half-and-half source (terminal-bench 10/20). `verified` is uniformly `false`
|
|
253
|
+
* today, so it is carried but not gated. */
|
|
254
|
+
export const FOCUS_SELF_REPORTED_MAX_SHARE = 0.5;
|
|
255
|
+
|
|
256
|
+
/** The per-entry org share above which a source is flagged as vendor-populated:
|
|
257
|
+
* one lab's own scores dominate the benchmark's entries. A **chosen, tunable
|
|
258
|
+
* heuristic** (a strict majority), not a measured value — the source's per-entry
|
|
259
|
+
* org mix is not measurable from the repo's caches at implementation time, so
|
|
260
|
+
* this bar is picked to flag a single-lab source while passing a mixed one. The
|
|
261
|
+
* axis is an annotation, never a gate: a `below-bar` provenance is warned but
|
|
262
|
+
* never drops a discovered candidate. */
|
|
263
|
+
export const FOCUS_PROVENANCE_DOMINANT_SHARE = 0.5;
|
|
264
|
+
|
|
265
|
+
/** The relative divergence above which the zeroeval per-entry price/context is
|
|
266
|
+
* flagged as disagreeing with the plugin's own value. A **chosen, tunable
|
|
267
|
+
* heuristic** (a factor-of-two band), not a measured value — the two sources are
|
|
268
|
+
* independent and a small divergence is expected (rounding, a different route
|
|
269
|
+
* mix). The axis is an annotation, never a gate. */
|
|
270
|
+
export const PRICE_AGREEMENT_TOLERANCE = 0.5;
|
|
271
|
+
|
|
272
|
+
/** The status vocabulary shared by every focus-metric axis. */
|
|
273
|
+
export type FocusSignal = "ok" | "below-bar" | "unknown";
|
|
274
|
+
|
|
275
|
+
/** Per-metric coverage of the ranking field (the coverage axis). */
|
|
276
|
+
export type FocusCoverageEntry = {
|
|
277
|
+
covered: number;
|
|
278
|
+
total: number | null;
|
|
279
|
+
share: number | null;
|
|
280
|
+
fill: number;
|
|
281
|
+
status: FocusSignal;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** The nine-axis assessment of one focus metric: coverage, dispersion, provider
|
|
285
|
+
* composition, freshness, source trust, modality, benchmark maintenance,
|
|
286
|
+
* provenance and cross-source agreement. The single object the gate and every
|
|
287
|
+
* report share. */
|
|
288
|
+
export type FocusMetricAssessment = {
|
|
289
|
+
coverage: FocusCoverageEntry;
|
|
290
|
+
dispersion: { value: number | null; status: FocusSignal };
|
|
291
|
+
composition: { omittedOrgs: string[]; status: FocusSignal };
|
|
292
|
+
freshness: { newestCovered: string | null; newestPool: string | null; monthsBehind: number | null; status: FocusSignal };
|
|
293
|
+
/** The source's per-entry trust: the self-reported share of the covered
|
|
294
|
+
* entries. Zeros + `unknown` when the payload carried no entry flags (a
|
|
295
|
+
* declared/writing source) or the metric's scores were not loaded. `community`
|
|
296
|
+
* is the catalog row's `is_community` flag: a community-submitted benchmark is
|
|
297
|
+
* still visible even when the payload half is absent (its scores carry no
|
|
298
|
+
* entry flags). It is an **annotation, never a gate** — it never changes
|
|
299
|
+
* `status` and `belowBarReason` has no community branch. */
|
|
300
|
+
trust: { selfReported: number; covered: number; status: FocusSignal; community: boolean };
|
|
301
|
+
/** The benchmark's modality: the catalog row's `modality` and the payload's
|
|
302
|
+
* multimodal share. Informational only — there is no role modality field, so
|
|
303
|
+
* this axis is never `below-bar` and never a drop reason; it is `unknown` only
|
|
304
|
+
* when neither the catalog row nor the payload carries a modality signal. */
|
|
305
|
+
modality: { value: string | null; multimodalShare: number | null; status: FocusSignal };
|
|
306
|
+
/** The benchmark's own maintenance: the catalog row's `updated_at` age in
|
|
307
|
+
* months against `FOCUS_DATASET_STALENESS_MONTHS`, with `version_count` and
|
|
308
|
+
* `star_count` carried as supporting detail. This is the **dataset-date** axis,
|
|
309
|
+
* distinct from `freshness` (the **model-date** axis): it measures whether the
|
|
310
|
+
* benchmark *project* is alive, not whether it tracks the model field. It is an
|
|
311
|
+
* **annotation, never a gate** — `belowBarReason` has no maintenance branch, so
|
|
312
|
+
* a stale dataset is warned but never drops a discovered candidate. `unknown`
|
|
313
|
+
* when the catalog row is absent or its `updated_at` is missing/unparseable. */
|
|
314
|
+
maintenance: { updatedAt: string | null; monthsOld: number | null; versionCount: number | null; starCount: number | null; status: FocusSignal };
|
|
315
|
+
/** The benchmark's provenance: the catalog row's owning organisation
|
|
316
|
+
* (`dataset_org_id`, falling back to `dataset_slug`) and the source's own
|
|
317
|
+
* per-entry org mix (the dominant lab and its share). This is the **source's
|
|
318
|
+
* own claim** about who publishes the benchmark and whose scores it carries —
|
|
319
|
+
* an independent cross-check on the pool-derived `composition` axis, not a
|
|
320
|
+
* replacement for it. `compositionAgreement` records whether the two concur:
|
|
321
|
+
* `"disagree"` when exactly one of the two flags `below-bar`, `"agree"` when
|
|
322
|
+
* both or neither do, `"unknown"` when the payload carries no org mix. It is an
|
|
323
|
+
* **annotation, never a gate** — `belowBarReason` has no provenance branch, so
|
|
324
|
+
* a vendor-populated source is warned but never drops a discovered candidate.
|
|
325
|
+
* `unknown` when there is neither an owner nor a payload org mix. */
|
|
326
|
+
provenance: {
|
|
327
|
+
owner: string | null;
|
|
328
|
+
dominantOrg: string | null;
|
|
329
|
+
dominantShare: number | null;
|
|
330
|
+
compositionAgreement: "agree" | "disagree" | "unknown";
|
|
331
|
+
status: FocusSignal;
|
|
332
|
+
};
|
|
333
|
+
/** The cross-source agreement: the zeroeval per-entry price/throughput/context
|
|
334
|
+
* (`BenchmarkPayloadMeta.crossSource`) compared against the plugin's own
|
|
335
|
+
* `price`/`throughput`/`context` for the models that carry the metric. The join
|
|
336
|
+
* is **direct on the bare `model_id`** (the generic llm-stats benchmark's
|
|
337
|
+
* `entries[].model_id` is the llm-stats id, the same space as `Model.id`).
|
|
338
|
+
* `compared` counts the models present in the map AND carrying the metric;
|
|
339
|
+
* `priceDivergence`/`contextDivergence` are the median relative divergence
|
|
340
|
+
* (`|a − b| / max(a, b, 1e-9)`) over the models compared on that axis, `null`
|
|
341
|
+
* when none. `priceAgreement`/`contextAgreement` are `unknown` when nothing was
|
|
342
|
+
* compared on the axis, else `ok` within `PRICE_AGREEMENT_TOLERANCE`, else
|
|
343
|
+
* `below-bar`. `speedAgreement` is **informational only** and excluded from
|
|
344
|
+
* `status`: the units differ (zeroeval `speed_rps` is requests/s, the plugin's
|
|
345
|
+
* `throughput` is output tok/s) and re-deriving throughput from `speed_rps` is
|
|
346
|
+
* explicitly out of scope. `status` is `unknown` when `compared === 0`, else
|
|
347
|
+
* `below-bar` when `priceAgreement` or `contextAgreement` is `below-bar`, else
|
|
348
|
+
* `ok`. It is an **annotation, never a gate** — `belowBarReason` has no
|
|
349
|
+
* cross-source branch, and the axis never feeds `price`/`weightPrice` or any
|
|
350
|
+
* ranking input. */
|
|
351
|
+
crossSource: {
|
|
352
|
+
compared: number;
|
|
353
|
+
priceAgreement: FocusSignal;
|
|
354
|
+
contextAgreement: FocusSignal;
|
|
355
|
+
speedAgreement: FocusSignal;
|
|
356
|
+
priceDivergence: number | null;
|
|
357
|
+
contextDivergence: number | null;
|
|
358
|
+
status: FocusSignal;
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** Per-metric coverage of the ranking field, for the focus-coverage gate. */
|
|
363
|
+
export type FocusCoverage = {
|
|
364
|
+
/** Total models in the ranking universe; null when the load failed. */
|
|
365
|
+
total: number | null;
|
|
366
|
+
/** metric -> models that actually carry it (not the imputed fill). */
|
|
367
|
+
covered: Record<string, number>;
|
|
368
|
+
/** Declared sources, so a declared metric's fill resolves. */
|
|
369
|
+
declared?: readonly SourceDeclaration[];
|
|
370
|
+
/** Nine-axis assessment per metric, when the caller assessed the loaded pool.
|
|
371
|
+
* Absent → the eight new axes are `unknown` (the metric's scores were not
|
|
372
|
+
* loaded), never a silent `ok`. */
|
|
373
|
+
assessments?: Record<string, FocusMetricAssessment>;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/** The capability fill a metric's absence is scored at (0 = a coverage penalty). */
|
|
377
|
+
export function metricFill(metric: string, declared: readonly SourceDeclaration[] = []): number {
|
|
378
|
+
return CAPABILITY_FILL[metric] ?? sourceForMetric(metric, declared)?.fill ?? 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The one coverage rule, shared by the discovery gate and the report annotation:
|
|
383
|
+
* a focus metric is safe when its source is capability-filled (`fill > 0`, so
|
|
384
|
+
* absence is a known non-penalty) or its coverage share clears
|
|
385
|
+
* `FOCUS_COVERAGE_FLOOR`. When the field size is unknown the share rule is
|
|
386
|
+
* skipped and the result is `unknown` (annotated, not warned).
|
|
387
|
+
*/
|
|
388
|
+
export function focusCoverageOk(
|
|
389
|
+
metric: string,
|
|
390
|
+
covered: number,
|
|
391
|
+
total: number | null,
|
|
392
|
+
declared: readonly SourceDeclaration[] = [],
|
|
393
|
+
): FocusSignal {
|
|
394
|
+
if (metricFill(metric, declared) > 0) return "ok";
|
|
395
|
+
if (total === null || total <= 0) return "unknown";
|
|
396
|
+
return covered / total >= FOCUS_COVERAGE_FLOOR ? "ok" : "below-bar";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Count the models a metric actually covers: a non-null value that is not the
|
|
400
|
+
* source's imputed fill. */
|
|
401
|
+
export function countMetricCoverage(models: readonly Model[], metric: string, declared: readonly SourceDeclaration[] = []): number {
|
|
402
|
+
const fill = metricFill(metric, declared);
|
|
403
|
+
let covered = 0;
|
|
404
|
+
for (const model of models) {
|
|
405
|
+
const value = model.metrics[metric];
|
|
406
|
+
if (value == null) continue;
|
|
407
|
+
if (fill > 0 && value === fill) continue;
|
|
408
|
+
covered++;
|
|
409
|
+
}
|
|
410
|
+
return covered;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The covered models' values for a metric, cardinal-normalized: the engine's
|
|
414
|
+
* transform first (index metrics are raw −16..+60, benchmarks 0–1), so one
|
|
415
|
+
* dispersion floor is comparable across metrics. The imputed fill is skipped. */
|
|
416
|
+
function coveredValues(models: readonly Model[], metric: string, fill: number): number[] {
|
|
417
|
+
const values: number[] = [];
|
|
418
|
+
for (const model of models) {
|
|
419
|
+
const value = model.metrics[metric];
|
|
420
|
+
if (value == null) continue;
|
|
421
|
+
if (fill > 0 && value === fill) continue;
|
|
422
|
+
values.push(cardinalMetric(metric, value));
|
|
423
|
+
}
|
|
424
|
+
return values;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Linear-interpolated quantile (R type 7) of a value list. */
|
|
428
|
+
function quantile(values: readonly number[], p: number): number {
|
|
429
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
430
|
+
const h = (sorted.length - 1) * p;
|
|
431
|
+
const lo = Math.floor(h);
|
|
432
|
+
const hi = Math.ceil(h);
|
|
433
|
+
return sorted[lo] + (h - lo) * (sorted[hi] - sorted[lo]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The dispersion axis: IQR/median of the cardinal-normalized covered values.
|
|
437
|
+
* Fewer than two values, or a zero median, is `unknown` (no scale to divide by). */
|
|
438
|
+
function dispersionAxis(models: readonly Model[], metric: string, fill: number): FocusMetricAssessment["dispersion"] {
|
|
439
|
+
const values = coveredValues(models, metric, fill);
|
|
440
|
+
if (values.length < 2) return { value: null, status: "unknown" };
|
|
441
|
+
const median = quantile(values, 0.5);
|
|
442
|
+
if (median === 0) return { value: null, status: "unknown" };
|
|
443
|
+
const value = (quantile(values, 0.75) - quantile(values, 0.25)) / median;
|
|
444
|
+
return { value, status: value >= FOCUS_DISPERSION_FLOOR ? "ok" : "below-bar" };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The composition axis: an org present in the pool above `FOCUS_ORG_MIN_SHARE`
|
|
448
|
+
* but absent from the covered set is flagged. Orgs below the minimum pool share
|
|
449
|
+
* are ignored (a small provider's absence is not a false positive). */
|
|
450
|
+
function compositionAxis(models: readonly Model[], metric: string, fill: number): FocusMetricAssessment["composition"] {
|
|
451
|
+
const poolCounts = new Map<string, number>();
|
|
452
|
+
const coveredCounts = new Map<string, number>();
|
|
453
|
+
for (const model of models) {
|
|
454
|
+
poolCounts.set(model.org, (poolCounts.get(model.org) ?? 0) + 1);
|
|
455
|
+
const value = model.metrics[metric];
|
|
456
|
+
if (value == null) continue;
|
|
457
|
+
if (fill > 0 && value === fill) continue;
|
|
458
|
+
coveredCounts.set(model.org, (coveredCounts.get(model.org) ?? 0) + 1);
|
|
459
|
+
}
|
|
460
|
+
const total = models.length;
|
|
461
|
+
const omittedOrgs: string[] = [];
|
|
462
|
+
for (const [org, count] of poolCounts) {
|
|
463
|
+
if (count / total < FOCUS_ORG_MIN_SHARE) continue;
|
|
464
|
+
if ((coveredCounts.get(org) ?? 0) === 0) omittedOrgs.push(org);
|
|
465
|
+
}
|
|
466
|
+
omittedOrgs.sort();
|
|
467
|
+
return { omittedOrgs, status: omittedOrgs.length === 0 ? "ok" : "below-bar" };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The freshness axis: the newest covered model's date against the pool's newest.
|
|
471
|
+
* A missing date on either side degrades to `unknown` (a sparse date field must
|
|
472
|
+
* not false-positive). */
|
|
473
|
+
function freshnessAxis(models: readonly Model[], metric: string, fill: number): FocusMetricAssessment["freshness"] {
|
|
474
|
+
let newestPool: string | null = null;
|
|
475
|
+
let newestCovered: string | null = null;
|
|
476
|
+
for (const model of models) {
|
|
477
|
+
const date = model.releaseDate;
|
|
478
|
+
if (date == null) continue;
|
|
479
|
+
if (newestPool === null || date > newestPool) newestPool = date;
|
|
480
|
+
const value = model.metrics[metric];
|
|
481
|
+
if (value == null) continue;
|
|
482
|
+
if (fill > 0 && value === fill) continue;
|
|
483
|
+
if (newestCovered === null || date > newestCovered) newestCovered = date;
|
|
484
|
+
}
|
|
485
|
+
if (newestPool === null || newestCovered === null) {
|
|
486
|
+
return { newestCovered, newestPool, monthsBehind: null, status: "unknown" };
|
|
487
|
+
}
|
|
488
|
+
// 30.4375 = 365.25/12 days per month, so a calendar-month gap reads as ~1.
|
|
489
|
+
const monthsBehind = (Date.parse(newestPool) - Date.parse(newestCovered)) / (1000 * 60 * 60 * 24 * 30.4375);
|
|
490
|
+
return { newestCovered, newestPool, monthsBehind, status: monthsBehind > FOCUS_STALENESS_MONTHS ? "below-bar" : "ok" };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** The trust axis: the self-reported share of the covered entries against
|
|
494
|
+
* `FOCUS_SELF_REPORTED_MAX_SHARE`. A payload with no entry flags (a declared or
|
|
495
|
+
* writing source) or no covered entries is `unknown` — never a silent `ok`. */
|
|
496
|
+
function trustAxis(source?: FocusSourceInfo): FocusMetricAssessment["trust"] {
|
|
497
|
+
const community = source?.catalog?.isCommunity ?? false;
|
|
498
|
+
const trust = source?.payload?.trust ?? null;
|
|
499
|
+
if (trust === null || trust.covered === 0) return { selfReported: 0, covered: 0, status: "unknown", community };
|
|
500
|
+
return {
|
|
501
|
+
selfReported: trust.selfReported,
|
|
502
|
+
covered: trust.covered,
|
|
503
|
+
status: trust.selfReported / trust.covered > FOCUS_SELF_REPORTED_MAX_SHARE ? "below-bar" : "ok",
|
|
504
|
+
community,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** The modality axis: the catalog row's `modality` and the payload's multimodal
|
|
509
|
+
* share. Informational only — there is no role modality field, so this axis is
|
|
510
|
+
* never `below-bar` and never a drop reason; it is `unknown` only when neither
|
|
511
|
+
* the catalog row nor the payload carries a modality signal. */
|
|
512
|
+
function modalityAxis(source?: FocusSourceInfo): FocusMetricAssessment["modality"] {
|
|
513
|
+
const value = source?.catalog?.modality ?? null;
|
|
514
|
+
const summary = source?.payload?.modality ?? null;
|
|
515
|
+
const multimodalShare = summary !== null && summary.covered > 0 ? summary.multimodal / summary.covered : null;
|
|
516
|
+
return { value, multimodalShare, status: value === null && multimodalShare === null ? "unknown" : "ok" };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** The maintenance axis: the catalog row's dataset age against
|
|
520
|
+
* `FOCUS_DATASET_STALENESS_MONTHS`, with `version_count`/`star_count` carried as
|
|
521
|
+
* supporting detail. This is the **dataset-date** axis, distinct from `freshness`
|
|
522
|
+
* (the **model-date** axis). It is an **annotation, never a gate**:
|
|
523
|
+
* `belowBarReason` has no maintenance branch, so a stale dataset is warned but
|
|
524
|
+
* never drops a discovered candidate. `unknown` when the catalog row is absent or
|
|
525
|
+
* its `updated_at` is missing/unparseable. */
|
|
526
|
+
function maintenanceAxis(source?: FocusSourceInfo): FocusMetricAssessment["maintenance"] {
|
|
527
|
+
const catalog = source?.catalog ?? null;
|
|
528
|
+
const updatedAt = catalog?.updatedAt ?? null;
|
|
529
|
+
const versionCount = catalog?.versionCount ?? null;
|
|
530
|
+
const starCount = catalog?.starCount ?? null;
|
|
531
|
+
const parsed = updatedAt === null ? Number.NaN : Date.parse(updatedAt);
|
|
532
|
+
if (Number.isNaN(parsed)) return { updatedAt, monthsOld: null, versionCount, starCount, status: "unknown" };
|
|
533
|
+
// 30.4375 = 365.25/12 days per month, matching the freshness axis.
|
|
534
|
+
const monthsOld = (Date.now() - parsed) / (1000 * 60 * 60 * 24 * 30.4375);
|
|
535
|
+
return { updatedAt, monthsOld, versionCount, starCount, status: monthsOld > FOCUS_DATASET_STALENESS_MONTHS ? "below-bar" : "ok" };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** The provenance axis: the benchmark's owner (the catalog row's `dataset_org_id`,
|
|
539
|
+
* falling back to `dataset_slug`) and the source's own per-entry org mix (the
|
|
540
|
+
* dominant lab and its share). This is the **source's own claim** about who
|
|
541
|
+
* publishes the benchmark and whose scores it carries — an independent
|
|
542
|
+
* cross-check on the pool-derived `composition` axis, not a replacement for it.
|
|
543
|
+
* `compositionAgreement` records whether the two concur: `"disagree"` when
|
|
544
|
+
* exactly one of the two flags `below-bar`, `"agree"` when both or neither do,
|
|
545
|
+
* `"unknown"` when the payload carries no org mix. It is an **annotation, never a
|
|
546
|
+
* gate**: `belowBarReason` has no provenance branch, so a vendor-populated source
|
|
547
|
+
* is warned but never drops a discovered candidate. `unknown` when there is
|
|
548
|
+
* neither an owner nor a payload org mix. */
|
|
549
|
+
function provenanceAxis(source: FocusSourceInfo | undefined, compositionStatus: FocusSignal): FocusMetricAssessment["provenance"] {
|
|
550
|
+
const owner = source?.catalog?.datasetOrgId ?? source?.catalog?.datasetSlug ?? null;
|
|
551
|
+
const mix = source?.payload?.provenance ?? null;
|
|
552
|
+
const dominantOrg = mix?.dominantOrg ?? null;
|
|
553
|
+
const dominantShare = mix?.dominantShare ?? null;
|
|
554
|
+
const status: FocusSignal =
|
|
555
|
+
owner === null && mix === null ? "unknown" : dominantShare !== null && dominantShare > FOCUS_PROVENANCE_DOMINANT_SHARE ? "below-bar" : "ok";
|
|
556
|
+
const compositionAgreement: "agree" | "disagree" | "unknown" =
|
|
557
|
+
mix === null ? "unknown" : (compositionStatus === "below-bar") !== (status === "below-bar") ? "disagree" : "agree";
|
|
558
|
+
return { owner, dominantOrg, dominantShare, compositionAgreement, status };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** The cross-source axis: the zeroeval per-entry price/throughput/context
|
|
562
|
+
* (`BenchmarkPayloadMeta.crossSource`, keyed by the entry's `model_id`) compared
|
|
563
|
+
* against the plugin's own `price`/`throughput`/`context`. The join is **direct
|
|
564
|
+
* on the bare id**: the generic llm-stats benchmark's `entries[].model_id` is the
|
|
565
|
+
* llm-stats id, the same space as `Model.id` (the same assumption
|
|
566
|
+
* `parseLlmStatsBenchmark`'s direct join makes). Only models that are BOTH
|
|
567
|
+
* present in the map AND carry the metric (`m.metrics[metric] != null` and not
|
|
568
|
+
* the fill) count toward `compared`.
|
|
569
|
+
*
|
|
570
|
+
* price compares zeroeval's 3:1 blend `(3·input + output)/4` against `m.price`
|
|
571
|
+
* ($/M); context compares `row.context` against `m.context`; speed compares
|
|
572
|
+
* `row.speed` (requests/s) against `m.throughput` (output tok/s). The relative
|
|
573
|
+
* divergence is `|a − b| / max(a, b, 1e-9)`; `priceDivergence`/
|
|
574
|
+
* `contextDivergence` are the median over the models compared on that axis
|
|
575
|
+
* (`null` when none). An axis is `unknown` when nothing was compared on it, else
|
|
576
|
+
* `ok` within `PRICE_AGREEMENT_TOLERANCE`, else `below-bar`.
|
|
577
|
+
*
|
|
578
|
+
* `speedAgreement` is **informational only** and excluded from `status`: the
|
|
579
|
+
* units differ (rps vs tok/s) and re-deriving throughput from `speed_rps` is
|
|
580
|
+
* explicitly out of scope. `status` is `unknown` when `compared === 0`, else
|
|
581
|
+
* `below-bar` when `priceAgreement` or `contextAgreement` is `below-bar`, else
|
|
582
|
+
* `ok`. It is an **annotation, never a gate** — `belowBarReason` has no
|
|
583
|
+
* cross-source branch, and the axis never feeds `price`/`weightPrice` or any
|
|
584
|
+
* ranking input. (A future option, recorded in the docs only: use zeroeval's
|
|
585
|
+
* list price as a fallback fill for a model with no OpenRouter route.) */
|
|
586
|
+
function crossSourceAxis(models: readonly Model[], metric: string, fill: number, source?: FocusSourceInfo): FocusMetricAssessment["crossSource"] {
|
|
587
|
+
const map = source?.payload?.crossSource ?? null;
|
|
588
|
+
const priceDivergences: number[] = [];
|
|
589
|
+
const contextDivergences: number[] = [];
|
|
590
|
+
const speedDivergences: number[] = [];
|
|
591
|
+
let compared = 0;
|
|
592
|
+
for (const model of models) {
|
|
593
|
+
const value = model.metrics[metric];
|
|
594
|
+
if (value == null) continue;
|
|
595
|
+
if (fill > 0 && value === fill) continue;
|
|
596
|
+
const row = map?.[model.id];
|
|
597
|
+
if (row === undefined) continue;
|
|
598
|
+
compared++;
|
|
599
|
+
if (row.input !== null && row.output !== null && model.price !== null) {
|
|
600
|
+
const zeroPrice = (3 * row.input + row.output) / 4;
|
|
601
|
+
priceDivergences.push(Math.abs(zeroPrice - model.price) / Math.max(zeroPrice, model.price, 1e-9));
|
|
602
|
+
}
|
|
603
|
+
if (row.context !== null && model.context !== null) {
|
|
604
|
+
contextDivergences.push(Math.abs(row.context - model.context) / Math.max(row.context, model.context, 1e-9));
|
|
605
|
+
}
|
|
606
|
+
if (row.speed !== null && model.throughput !== null) {
|
|
607
|
+
speedDivergences.push(Math.abs(row.speed - model.throughput) / Math.max(row.speed, model.throughput, 1e-9));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const priceDivergence = priceDivergences.length === 0 ? null : quantile(priceDivergences, 0.5);
|
|
611
|
+
const contextDivergence = contextDivergences.length === 0 ? null : quantile(contextDivergences, 0.5);
|
|
612
|
+
const speedDivergence = speedDivergences.length === 0 ? null : quantile(speedDivergences, 0.5);
|
|
613
|
+
const priceAgreement: FocusSignal = priceDivergence === null ? "unknown" : priceDivergence <= PRICE_AGREEMENT_TOLERANCE ? "ok" : "below-bar";
|
|
614
|
+
const contextAgreement: FocusSignal = contextDivergence === null ? "unknown" : contextDivergence <= PRICE_AGREEMENT_TOLERANCE ? "ok" : "below-bar";
|
|
615
|
+
const speedAgreement: FocusSignal = speedDivergence === null ? "unknown" : speedDivergence <= PRICE_AGREEMENT_TOLERANCE ? "ok" : "below-bar";
|
|
616
|
+
const status: FocusSignal = compared === 0 ? "unknown" : priceAgreement === "below-bar" || contextAgreement === "below-bar" ? "below-bar" : "ok";
|
|
617
|
+
return { compared, priceAgreement, contextAgreement, speedAgreement, priceDivergence, contextDivergence, status };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** The all-unknown assessment: the field size is unknown, or the metric's scores
|
|
621
|
+
* were not loaded. Annotated, never warned. */
|
|
622
|
+
function unknownAssessment(coverage: FocusCoverageEntry, source?: FocusSourceInfo): FocusMetricAssessment {
|
|
623
|
+
return {
|
|
624
|
+
coverage,
|
|
625
|
+
dispersion: { value: null, status: "unknown" },
|
|
626
|
+
composition: { omittedOrgs: [], status: "unknown" },
|
|
627
|
+
freshness: { newestCovered: null, newestPool: null, monthsBehind: null, status: "unknown" },
|
|
628
|
+
trust: { selfReported: 0, covered: 0, status: "unknown", community: source?.catalog?.isCommunity ?? false },
|
|
629
|
+
modality: { value: null, multimodalShare: null, status: "unknown" },
|
|
630
|
+
maintenance: { updatedAt: null, monthsOld: null, versionCount: null, starCount: null, status: "unknown" },
|
|
631
|
+
provenance: { owner: null, dominantOrg: null, dominantShare: null, compositionAgreement: "unknown", status: "unknown" },
|
|
632
|
+
crossSource: { compared: 0, priceAgreement: "unknown", contextAgreement: "unknown", speedAgreement: "unknown", priceDivergence: null, contextDivergence: null, status: "unknown" },
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* The one assessment rule, shared by the discovery gate, the report and the
|
|
638
|
+
* explorer: coverage, dispersion, provider composition, freshness, source trust,
|
|
639
|
+
* modality, benchmark maintenance, provenance and cross-source agreement of one
|
|
640
|
+
* focus metric over the loaded pool. `focusCoverageOk`/`countMetricCoverage` are
|
|
641
|
+
* its coverage branch; `source` (the cached trust, modality, catalog, provenance
|
|
642
|
+
* and cross-source inputs) is its trust/modality/maintenance/provenance/
|
|
643
|
+
* cross-source branch.
|
|
644
|
+
*
|
|
645
|
+
* An empty pool (unknown field size) or a metric with no covered values (its
|
|
646
|
+
* scores were not loaded) reports every axis `unknown` — never a silent `ok`.
|
|
647
|
+
*/
|
|
648
|
+
export function assessFocusMetric(
|
|
649
|
+
models: readonly Model[],
|
|
650
|
+
metric: string,
|
|
651
|
+
declared: readonly SourceDeclaration[] = [],
|
|
652
|
+
source?: FocusSourceInfo,
|
|
653
|
+
): FocusMetricAssessment {
|
|
654
|
+
const total = models.length;
|
|
655
|
+
const covered = countMetricCoverage(models, metric, declared);
|
|
656
|
+
const fill = metricFill(metric, declared);
|
|
657
|
+
const coverage: FocusCoverageEntry = {
|
|
658
|
+
covered,
|
|
659
|
+
total: total > 0 ? total : null,
|
|
660
|
+
share: total > 0 ? covered / total : null,
|
|
661
|
+
fill,
|
|
662
|
+
status: focusCoverageOk(metric, covered, total > 0 ? total : null, declared),
|
|
663
|
+
};
|
|
664
|
+
if (total === 0 || covered === 0) return unknownAssessment({ ...coverage, status: "unknown" }, source);
|
|
665
|
+
const composition = compositionAxis(models, metric, fill);
|
|
666
|
+
return {
|
|
667
|
+
coverage,
|
|
668
|
+
dispersion: dispersionAxis(models, metric, fill),
|
|
669
|
+
composition,
|
|
670
|
+
freshness: freshnessAxis(models, metric, fill),
|
|
671
|
+
trust: trustAxis(source),
|
|
672
|
+
modality: modalityAxis(source),
|
|
673
|
+
maintenance: maintenanceAxis(source),
|
|
674
|
+
provenance: provenanceAxis(source, composition.status),
|
|
675
|
+
crossSource: crossSourceAxis(models, metric, fill, source),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** The one-line reason a below-bar assessment is dropped/warned, naming the
|
|
680
|
+
* failed axis; null when every axis is ok or unknown. Deliberately has **no
|
|
681
|
+
* maintenance, provenance or cross-source branch**: those axes are annotations,
|
|
682
|
+
* never drop reasons, so a stale dataset, a vendor-populated source or a
|
|
683
|
+
* price/context divergence cannot remove a discovered candidate. */
|
|
684
|
+
export function belowBarReason(assessment: FocusMetricAssessment): string | null {
|
|
685
|
+
const { coverage, dispersion, composition, freshness, trust } = assessment;
|
|
686
|
+
if (coverage.status === "below-bar") {
|
|
687
|
+
return `coverage ${coverage.covered}/${coverage.total} is below the ${Math.round(FOCUS_COVERAGE_FLOOR * 100)}% bar`;
|
|
688
|
+
}
|
|
689
|
+
if (dispersion.status === "below-bar") {
|
|
690
|
+
return `dispersion ${dispersion.value?.toFixed(3)} below the ${FOCUS_DISPERSION_FLOOR} bar`;
|
|
691
|
+
}
|
|
692
|
+
if (composition.status === "below-bar") {
|
|
693
|
+
return `composition omits ${composition.omittedOrgs.join(", ")}`;
|
|
694
|
+
}
|
|
695
|
+
if (freshness.status === "below-bar") {
|
|
696
|
+
return `freshness ${freshness.monthsBehind?.toFixed(1)} months behind the pool`;
|
|
697
|
+
}
|
|
698
|
+
if (trust.status === "below-bar") {
|
|
699
|
+
return `trust ${trust.selfReported}/${trust.covered} self-reported is above the ${FOCUS_SELF_REPORTED_MAX_SHARE} bar`;
|
|
700
|
+
}
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
197
704
|
/**
|
|
198
705
|
* Fold user-named benchmarks into a role's weights as a decisive *focus* share.
|
|
199
706
|
*
|
|
@@ -211,8 +718,19 @@ const FOCUS_SHARE_CAP = 0.4;
|
|
|
211
718
|
* known-metric set (a shipped key or an external `<ns>:<local>`) is unknown.
|
|
212
719
|
* Naming the archetype's own specialist set is a no-op (the focus share equals
|
|
213
720
|
* its archetype share). Nothing changes when the list is empty.
|
|
721
|
+
*
|
|
722
|
+
* The focus set is capped at `FOCUS_METRIC_CAP` in priority order (named/linked
|
|
723
|
+
* first, then discovery order), so the caller's explicit choices survive and one
|
|
724
|
+
* benchmark keeps a decisive share; the metrics beyond the cap are reported as
|
|
725
|
+
* `dropped`. `coverage.assessments` (the nine-axis assessment the caller computed
|
|
726
|
+
* over the loaded pool) drives the per-metric annotation; without it the eight
|
|
727
|
+
* new axes are `unknown` (the metric's scores were not loaded).
|
|
214
728
|
*/
|
|
215
|
-
export function applyFocusBenchmarks(
|
|
729
|
+
export function applyFocusBenchmarks(
|
|
730
|
+
weights: Record<string, number>,
|
|
731
|
+
metrics: readonly string[],
|
|
732
|
+
coverage?: FocusCoverage,
|
|
733
|
+
): BenchmarkApplication {
|
|
216
734
|
const added: string[] = [];
|
|
217
735
|
const duplicates: string[] = [];
|
|
218
736
|
const unknown: string[] = [];
|
|
@@ -225,22 +743,45 @@ export function applyFocusBenchmarks(weights: Record<string, number>, metrics: r
|
|
|
225
743
|
continue;
|
|
226
744
|
}
|
|
227
745
|
if (focus.includes(metric)) continue; // the same metric named twice
|
|
746
|
+
focus.push(metric);
|
|
747
|
+
}
|
|
748
|
+
const kept = focus.slice(0, FOCUS_METRIC_CAP);
|
|
749
|
+
const dropped = focus.slice(FOCUS_METRIC_CAP);
|
|
750
|
+
for (const metric of kept) {
|
|
228
751
|
if (metric in weights) duplicates.push(metric);
|
|
229
752
|
else added.push(metric);
|
|
230
|
-
focus.push(metric);
|
|
231
753
|
}
|
|
232
|
-
|
|
754
|
+
|
|
755
|
+
const declared = coverage?.declared ?? [];
|
|
756
|
+
const total = coverage?.total ?? null;
|
|
757
|
+
const assessments: Record<string, FocusMetricAssessment> = {};
|
|
758
|
+
for (const metric of kept) {
|
|
759
|
+
const assessed = coverage?.assessments?.[metric];
|
|
760
|
+
if (assessed !== undefined) {
|
|
761
|
+
assessments[metric] = assessed;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const covered = coverage?.covered[metric] ?? 0;
|
|
765
|
+
assessments[metric] = unknownAssessment({
|
|
766
|
+
covered,
|
|
767
|
+
total,
|
|
768
|
+
share: total !== null && total > 0 ? covered / total : null,
|
|
769
|
+
fill: metricFill(metric, declared),
|
|
770
|
+
status: focusCoverageOk(metric, covered, total, declared),
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (kept.length === 0) return { weights: { ...weights }, added, duplicates, unknown, dropped, assessments };
|
|
233
774
|
|
|
234
775
|
const price = weights.price ?? 0;
|
|
235
776
|
const specialistShare = Object.keys(weights)
|
|
236
777
|
.filter((key) => key !== "price" && !BACKBONE_METRICS[key])
|
|
237
778
|
.reduce((sum, key) => sum + weights[key], 0);
|
|
238
779
|
const focusShare = Math.min(FOCUS_SHARE_CAP, Math.max(FOCUS_SHARE_FLOOR, specialistShare));
|
|
239
|
-
const perFocus = focusShare /
|
|
780
|
+
const perFocus = focusShare / kept.length;
|
|
240
781
|
|
|
241
782
|
const next: Record<string, number> = { ...weights };
|
|
242
|
-
for (const metric of
|
|
243
|
-
const rest = Object.keys(next).filter((key) => key !== "price" && !
|
|
783
|
+
for (const metric of kept) next[metric] = perFocus;
|
|
784
|
+
const rest = Object.keys(next).filter((key) => key !== "price" && !kept.includes(key));
|
|
244
785
|
const restSum = rest.reduce((sum, key) => sum + next[key], 0);
|
|
245
786
|
const target = 1 - price - focusShare;
|
|
246
787
|
if (restSum > 0) for (const key of rest) next[key] = (next[key] * target) / restSum;
|
|
@@ -256,11 +797,27 @@ export function applyFocusBenchmarks(weights: Record<string, number>, metrics: r
|
|
|
256
797
|
const largest = keys.reduce((a, b) => (next[a] >= next[b] ? a : b));
|
|
257
798
|
next[largest] = Math.round((next[largest] + residual) * 1e4) / 1e4;
|
|
258
799
|
}
|
|
259
|
-
return { weights: next, added, duplicates, unknown };
|
|
800
|
+
return { weights: next, added, duplicates, unknown, dropped, assessments };
|
|
260
801
|
}
|
|
261
802
|
|
|
262
|
-
/** A benchmark discovered for a purpose: the metric it feeds
|
|
263
|
-
|
|
803
|
+
/** A benchmark discovered for a purpose: the metric it feeds, its label, the
|
|
804
|
+
* catalog's model count (the pre-fetch coverage numerator for the report
|
|
805
|
+
* annotation), and — for an id whose raw form the metric key cannot reconstruct
|
|
806
|
+
* (a dotted id) — the declaration the caller must persist so the updater can
|
|
807
|
+
* re-fetch it. */
|
|
808
|
+
export type DiscoveredBenchmark = { metric: string; label: string; covered: number; declaration?: SourceDeclaration };
|
|
809
|
+
|
|
810
|
+
/** A candidate the discovery gate dropped, with the reason (for the report). */
|
|
811
|
+
export type DroppedBenchmark = { metric: string; label: string; reason: string };
|
|
812
|
+
|
|
813
|
+
/** The discovery outcome: the benchmarks to fold in, and the ones the coverage
|
|
814
|
+
* gate dropped (non-fatal, reported). */
|
|
815
|
+
export type DiscoveryOutcome = { discovered: DiscoveredBenchmark[]; dropped: DroppedBenchmark[] };
|
|
816
|
+
|
|
817
|
+
/** The post-fetch assessor the discovery gate injects: probe-fetch a candidate's
|
|
818
|
+
* source, join it onto the loaded pool and assess it. The extension supplies the
|
|
819
|
+
* real one (via `loadBenchmarkScores`); tests inject a stub. */
|
|
820
|
+
export type FocusAssessor = (metric: string) => Promise<FocusMetricAssessment>;
|
|
264
821
|
|
|
265
822
|
/** Catalog benchmarks with fewer models than this are skipped (a 1-model
|
|
266
823
|
* benchmark would distort the ranking). */
|
|
@@ -329,28 +886,59 @@ function rankCatalogCandidates(purpose: string, candidates: readonly BenchmarkCa
|
|
|
329
886
|
* capped (the catalog is alphabetical, so a blind cap would drop late-sorting
|
|
330
887
|
* benchmarks), a selected benchmark resolves to its shipped metric when one
|
|
331
888
|
* exists (else `bench:<id>`), and a metric already in `exclude` is skipped.
|
|
889
|
+
*
|
|
890
|
+
* `fieldSize` is the ranking universe's model count (null when unknown). A
|
|
891
|
+
* selected benchmark whose metric is fill-0 and whose catalog coverage is below
|
|
892
|
+
* `FOCUS_COVERAGE_FLOOR` of the field is dropped (non-fatal) with a reason, so a
|
|
893
|
+
* sparse pass-rate benchmark cannot turn `q` into a coverage score. When the
|
|
894
|
+
* field size is unknown the share rule is skipped and only the fill rule applies.
|
|
895
|
+
*
|
|
896
|
+
* `assess` is the post-fetch gate: the create-agent pool carries no `bench:<id>`
|
|
897
|
+
* scores, so the caller probe-fetches each survivor's source and assesses the
|
|
898
|
+
* joined pool. A survivor below-bar on any axis is dropped with a reason naming
|
|
899
|
+
* the failed axis. Without `assess` only the pre-fetch count gate runs (the
|
|
900
|
+
* catalog has no scores to assess).
|
|
332
901
|
*/
|
|
333
902
|
export async function discoverBenchmarks(
|
|
334
903
|
purpose: string,
|
|
335
904
|
catalog: readonly BenchmarkCatalogEntry[],
|
|
336
905
|
decide: (purpose: string, candidates: readonly BenchmarkCatalogEntry[]) => Promise<readonly string[]>,
|
|
337
906
|
exclude: readonly string[] = [],
|
|
338
|
-
|
|
907
|
+
fieldSize: number | null = null,
|
|
908
|
+
assess?: FocusAssessor,
|
|
909
|
+
): Promise<DiscoveryOutcome> {
|
|
339
910
|
const candidates = catalog.filter((entry) => entry.modelCount >= MIN_CATALOG_MODELS);
|
|
340
|
-
if (candidates.length === 0) return [];
|
|
911
|
+
if (candidates.length === 0) return { discovered: [], dropped: [] };
|
|
341
912
|
const ranked = rankCatalogCandidates(purpose, candidates);
|
|
342
|
-
if (ranked.length === 0) return [];
|
|
913
|
+
if (ranked.length === 0) return { discovered: [], dropped: [] };
|
|
343
914
|
const selected = new Set(await decide(purpose, ranked));
|
|
344
915
|
const seen = new Set(exclude);
|
|
345
|
-
const
|
|
916
|
+
const discovered: DiscoveredBenchmark[] = [];
|
|
917
|
+
const dropped: DroppedBenchmark[] = [];
|
|
346
918
|
for (const entry of ranked) {
|
|
347
919
|
if (!selected.has(entry.id)) continue;
|
|
348
920
|
const metric = catalogMetric(entry.id);
|
|
349
921
|
if (seen.has(metric)) continue;
|
|
350
922
|
seen.add(metric);
|
|
351
|
-
|
|
923
|
+
if (focusCoverageOk(metric, entry.modelCount, fieldSize) === "below-bar") {
|
|
924
|
+
dropped.push({
|
|
925
|
+
metric,
|
|
926
|
+
label: entry.name,
|
|
927
|
+
reason: `coverage ${entry.modelCount}/${fieldSize} is below the ${Math.round(FOCUS_COVERAGE_FLOOR * 100)}% bar`,
|
|
928
|
+
});
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (assess !== undefined) {
|
|
932
|
+
const reason = belowBarReason(await assess(metric));
|
|
933
|
+
if (reason !== null) {
|
|
934
|
+
dropped.push({ metric, label: entry.name, reason });
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const declaration = catalogBenchmarkDeclaration(entry.id);
|
|
939
|
+
discovered.push({ metric, label: entry.name, covered: entry.modelCount, ...(declaration !== null ? { declaration } : {}) });
|
|
352
940
|
}
|
|
353
|
-
return
|
|
941
|
+
return { discovered, dropped };
|
|
354
942
|
}
|
|
355
943
|
|
|
356
944
|
/**
|
|
@@ -372,8 +960,12 @@ export function formatBenchmarks(weights?: Record<string, number>, extra?: reado
|
|
|
372
960
|
/** Shared empty match list for an explicitly chosen archetype (no keyword hits). */
|
|
373
961
|
const EMPTY_MATCHED: string[] = [];
|
|
374
962
|
|
|
375
|
-
/**
|
|
376
|
-
|
|
963
|
+
/**
|
|
964
|
+
* Resolve the archetype, then layer the caller's explicit overrides on top.
|
|
965
|
+
* Exported so the extension can preview the request's weights (for the live
|
|
966
|
+
* "already in this role's weights" marker) without writing anything.
|
|
967
|
+
*/
|
|
968
|
+
export function resolveRole(request: CreateAgentRequest): { archetype: Archetype; matched: string[]; def: RoleDef } | { errors: string[] } {
|
|
377
969
|
const match = request.archetypeId === undefined ? fitArchetype(stripBenchmarkLinks(request.purpose)) : { archetype: archetypeById(request.archetypeId), matched: EMPTY_MATCHED };
|
|
378
970
|
if (match.archetype === null) {
|
|
379
971
|
return { errors: [`unknown archetype "${request.archetypeId}" — one of ${ARCHETYPES.map((a) => a.id).join(", ")}`] };
|
|
@@ -385,12 +977,27 @@ function resolveRole(request: CreateAgentRequest): { archetype: Archetype; match
|
|
|
385
977
|
const thinking = request.thinking ?? archetype.thinking;
|
|
386
978
|
if (thinking !== undefined) def.thinking = thinking;
|
|
387
979
|
if (request.image === true || (request.image === undefined && archetype.image === true)) def.filters = { image: true };
|
|
980
|
+
// The request's flags win; otherwise the archetype's own list (no shipped
|
|
981
|
+
// archetype sets one, so a plugin-created role starts from nothing unless the
|
|
982
|
+
// user asks). The flag is authored, not expanded — `resolveSettings` expands it.
|
|
983
|
+
let features: Record<string, boolean> | undefined = request.features;
|
|
984
|
+
if (features === undefined && archetype.features !== undefined && archetype.features.length > 0) {
|
|
985
|
+
features = {};
|
|
986
|
+
for (const id of archetype.features) features[id] = true;
|
|
987
|
+
}
|
|
988
|
+
if (features !== undefined) def.features = features;
|
|
388
989
|
return { archetype, matched: match.matched, def };
|
|
389
990
|
}
|
|
390
991
|
|
|
391
992
|
/**
|
|
392
993
|
* Create the role and the agent file for `request`. Nothing is written when any
|
|
393
994
|
* check fails; `dryRun` runs every check and stops before the writes.
|
|
995
|
+
*
|
|
996
|
+
* The agent file is written **before** the role: an agent with no role is
|
|
997
|
+
* harmless (its `@<name>, @default` chain falls back to `@default`), while a
|
|
998
|
+
* role with no agent is a ranked-but-dead entry the updater would still wire. If
|
|
999
|
+
* the role write fails after the agent file landed, the agent file is removed
|
|
1000
|
+
* (rollback), so a retry needs no `--force`.
|
|
394
1001
|
*/
|
|
395
1002
|
export function createAgent(request: CreateAgentRequest): CreateAgentResult {
|
|
396
1003
|
const errors: string[] = [];
|
|
@@ -404,7 +1011,7 @@ export function createAgent(request: CreateAgentRequest): CreateAgentResult {
|
|
|
404
1011
|
|
|
405
1012
|
const resolved = resolveRole(request);
|
|
406
1013
|
if ("errors" in resolved) return { ok: false, errors: [...errors, ...resolved.errors] };
|
|
407
|
-
const bench = applyFocusBenchmarks(resolved.def.weights, request.extraBenchmarks ?? []);
|
|
1014
|
+
const bench = applyFocusBenchmarks(resolved.def.weights, request.extraBenchmarks ?? [], request.coverage);
|
|
408
1015
|
resolved.def.weights = bench.weights;
|
|
409
1016
|
errors.push(...checkWeightMath(name, resolved.def.weights, resolved.def.required));
|
|
410
1017
|
|
|
@@ -413,8 +1020,8 @@ export function createAgent(request: CreateAgentRequest): CreateAgentResult {
|
|
|
413
1020
|
const agentsDir = request.scope === "project" ? projectAgentsDir(request.projectAnchor ?? process.cwd()) : userAgentsDir();
|
|
414
1021
|
const agentPath = join(agentsDir, `${name}.md`);
|
|
415
1022
|
const model = `@${name}, @default`;
|
|
416
|
-
// Pre-flight: the
|
|
417
|
-
// refused, or a failed run leaves a role
|
|
1023
|
+
// Pre-flight: the agent file must not be written when the role is going to be
|
|
1024
|
+
// refused, or a failed run leaves an agent behind for a role that never lands.
|
|
418
1025
|
if (existsSync(agentPath) && !request.force) errors.push(`${agentPath} already exists — pass --force to overwrite`);
|
|
419
1026
|
if (errors.length > 0) return { ok: false, errors };
|
|
420
1027
|
|
|
@@ -425,23 +1032,68 @@ export function createAgent(request: CreateAgentRequest): CreateAgentResult {
|
|
|
425
1032
|
return { ok: true, name, archetype: resolved.archetype, matched: resolved.matched, def: resolved.def, agentPath, model, readOnly, backupPath: null, dryRun: true, bench };
|
|
426
1033
|
}
|
|
427
1034
|
|
|
428
|
-
const written = writeRoleSettings(request.lockPath, { [name]: resolved.def });
|
|
429
|
-
if (!written.ok) return { ok: false, errors: written.errors };
|
|
430
|
-
|
|
431
1035
|
// omp's architect supplies the routing rule and the body; the plugin adds the
|
|
432
1036
|
// `model:`/`tools:` frontmatter omp's own writer omits. Without a spec (the
|
|
433
1037
|
// CLI, or a caller that skipped the architect) the archetype template is used.
|
|
434
1038
|
const description = request.spec?.whenToUse ?? routingDescription(request.purpose);
|
|
435
1039
|
const body = request.spec?.systemPrompt ?? buildBody(request, resolved.archetype, readOnly);
|
|
436
1040
|
const text = renderAgentFile({ name, description, model, tools, body });
|
|
1041
|
+
// Capture the prior text (only reachable with `--force`) so a failed role write
|
|
1042
|
+
// restores it instead of destroying the user's file and leaving the old role
|
|
1043
|
+
// dangling.
|
|
1044
|
+
const priorAgent = existsSync(agentPath) ? readFileSync(agentPath, "utf8") : null;
|
|
437
1045
|
const agent = writeAgentFile(agentPath, text, request.force);
|
|
438
|
-
if (!agent.ok) {
|
|
439
|
-
|
|
1046
|
+
if (!agent.ok) return { ok: false, errors: [agent.error] };
|
|
1047
|
+
|
|
1048
|
+
// The real role write can return `{ ok: false }` (validation/lock/merge/mtime)
|
|
1049
|
+
// or throw (a filesystem error in the atomic writer), so both paths roll the
|
|
1050
|
+
// agent file back — a retry then needs no `--force`.
|
|
1051
|
+
let written: RoleWriteResult;
|
|
1052
|
+
try {
|
|
1053
|
+
written = writeRoleSettings(request.lockPath, { [name]: resolved.def });
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
return { ok: false, errors: [rollbackAgent(agentPath, priorAgent, err instanceof Error ? err.message : String(err))] };
|
|
440
1056
|
}
|
|
1057
|
+
if (!written.ok) return { ok: false, errors: [rollbackAgent(agentPath, priorAgent, written.errors.join("; "))] };
|
|
441
1058
|
|
|
442
1059
|
return { ok: true, name, archetype: resolved.archetype, matched: resolved.matched, def: resolved.def, agentPath, model, readOnly, backupPath: written.backupPath, dryRun: false, bench };
|
|
443
1060
|
}
|
|
444
1061
|
|
|
1062
|
+
/**
|
|
1063
|
+
* Undo the agent write after a failed role write: restore the prior file when one
|
|
1064
|
+
* existed (a `--force` re-create), else remove the new one. Blind removal under
|
|
1065
|
+
* `--force` would destroy the user's agent and leave the old role dangling.
|
|
1066
|
+
*/
|
|
1067
|
+
function rollbackAgent(agentPath: string, priorAgent: string | null, roleError: string): string {
|
|
1068
|
+
if (priorAgent === null) {
|
|
1069
|
+
const removed = removeAgentFile(agentPath);
|
|
1070
|
+
const note = removed.ok ? "the agent file was removed" : `removing the agent file failed: ${removed.error}`;
|
|
1071
|
+
return `the agent file ${agentPath} was written, but the role was not: ${roleError} (${note})`;
|
|
1072
|
+
}
|
|
1073
|
+
const restored = writeAgentFile(agentPath, priorAgent, true);
|
|
1074
|
+
const note = restored.ok ? "the prior agent file was restored" : `restoring the prior agent file failed: ${restored.error}`;
|
|
1075
|
+
return `the agent file ${agentPath} was written, but the role was not: ${roleError} (${note})`;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* A warning when the new role's top pick equals the `default` role's top pick —
|
|
1080
|
+
* the role adds nothing over the default. `null` when they differ or the pool is
|
|
1081
|
+
* empty (nothing to compare).
|
|
1082
|
+
*/
|
|
1083
|
+
export function differentiationWarning(
|
|
1084
|
+
newDef: RoleDef,
|
|
1085
|
+
defaultDef: RoleDef,
|
|
1086
|
+
models: readonly Model[],
|
|
1087
|
+
): string | null {
|
|
1088
|
+
if (models.length === 0) return null;
|
|
1089
|
+
const pool = models as Model[];
|
|
1090
|
+
const next = rankRole(newDef, pool);
|
|
1091
|
+
const base = rankRole(defaultDef, pool);
|
|
1092
|
+
if (next.length === 0 || base.length === 0) return null;
|
|
1093
|
+
if (next[0].model.id !== base[0].model.id) return null;
|
|
1094
|
+
return `the new role's leader (${next[0].model.id}) is also the default role's leader — the role adds nothing; raise its distinctive metric or drop it`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
445
1097
|
/** The ok branch of a create run, for the report formatters. */
|
|
446
1098
|
export type CreatedAgent = Extract<CreateAgentResult, { ok: true }>;
|
|
447
1099
|
|
|
@@ -468,8 +1120,11 @@ export const CREATE_AGENT_USAGE = [
|
|
|
468
1120
|
" --thinking <level> off|minimal|low|medium|high|xhigh|max|auto",
|
|
469
1121
|
" --tools <a,b,...> builtin tool allowlist (default: the archetype's)",
|
|
470
1122
|
" --benchmarks <m,...> extra benchmarks to fold into the weights (see --list-benchmarks)",
|
|
1123
|
+
" --feature <id,...> capability flag(s) to author on the role (see --list-features);",
|
|
1124
|
+
" repeatable, comma-separated, merged",
|
|
471
1125
|
" --no-discover skip benchmark discovery (rank only on named benchmarks)",
|
|
472
1126
|
" --list-benchmarks print the weightable benchmarks and exit",
|
|
1127
|
+
" --list-features print the capability flags and the settings each applies, then exit",
|
|
473
1128
|
" --scope <user|project> where the agent file goes (default: user)",
|
|
474
1129
|
" --image require image input (filters.image)",
|
|
475
1130
|
" --body <text> override the generated agent body",
|
|
@@ -533,13 +1188,13 @@ function parseList(spec: string): string[] {
|
|
|
533
1188
|
}
|
|
534
1189
|
|
|
535
1190
|
export type ParsedCreateAgentArgs =
|
|
536
|
-
| { ok: true; request: CreateAgentRequest; json: boolean; bodyFile: string | undefined; listArchetypes: boolean; listBenchmarks: boolean; help: boolean; freeText: boolean; yes: boolean; noDiscover: boolean; explicitBenchmarks: boolean }
|
|
1191
|
+
| { ok: true; request: CreateAgentRequest; json: boolean; bodyFile: string | undefined; listArchetypes: boolean; listBenchmarks: boolean; listFeatures: boolean; help: boolean; freeText: boolean; yes: boolean; noDiscover: boolean; explicitBenchmarks: boolean }
|
|
537
1192
|
| { ok: false; error: string };
|
|
538
1193
|
|
|
539
1194
|
/** The flag half of the parse, without the required-name/purpose check (the
|
|
540
1195
|
* free-text form supplies those itself). */
|
|
541
1196
|
type ParsedFlags =
|
|
542
|
-
| { ok: true; request: CreateAgentRequest; json: boolean; bodyFile: string | undefined; listArchetypes: boolean; listBenchmarks: boolean; help: boolean; yes: boolean; noDiscover: boolean; explicitBenchmarks: boolean }
|
|
1197
|
+
| { ok: true; request: CreateAgentRequest; json: boolean; bodyFile: string | undefined; listArchetypes: boolean; listBenchmarks: boolean; listFeatures: boolean; help: boolean; yes: boolean; noDiscover: boolean; explicitBenchmarks: boolean }
|
|
543
1198
|
| { ok: false; error: string };
|
|
544
1199
|
|
|
545
1200
|
function parseFlags(argv: string[]): ParsedFlags {
|
|
@@ -555,6 +1210,7 @@ function parseFlags(argv: string[]): ParsedFlags {
|
|
|
555
1210
|
let bodyFile: string | undefined;
|
|
556
1211
|
let listArchetypes = false;
|
|
557
1212
|
let listBenchmarks = false;
|
|
1213
|
+
let listFeatures = false;
|
|
558
1214
|
let help = false;
|
|
559
1215
|
let yes = false;
|
|
560
1216
|
let noDiscover = false;
|
|
@@ -574,6 +1230,10 @@ function parseFlags(argv: string[]): ParsedFlags {
|
|
|
574
1230
|
listBenchmarks = true;
|
|
575
1231
|
continue;
|
|
576
1232
|
}
|
|
1233
|
+
if (flag === "--list-features") {
|
|
1234
|
+
listFeatures = true;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
577
1237
|
if (flag === "--force") {
|
|
578
1238
|
request.force = true;
|
|
579
1239
|
continue;
|
|
@@ -613,6 +1273,17 @@ function parseFlags(argv: string[]): ParsedFlags {
|
|
|
613
1273
|
else if (flag === "--benchmarks") {
|
|
614
1274
|
explicitBenchmarks = true;
|
|
615
1275
|
request.extraBenchmarks = parseList(value);
|
|
1276
|
+
} else if (flag === "--feature") {
|
|
1277
|
+
const ids = parseList(value);
|
|
1278
|
+
if (ids.length === 0) return { ok: false, error: `--feature: no capability id given\n\n${CREATE_AGENT_USAGE}` };
|
|
1279
|
+
const features = request.features ?? {};
|
|
1280
|
+
for (const id of ids) {
|
|
1281
|
+
if (featureById(id) === null) {
|
|
1282
|
+
return { ok: false, error: `--feature: unknown capability "${id}" — one of ${FEATURES.map((f) => f.id).join(", ")}\n\n${CREATE_AGENT_USAGE}` };
|
|
1283
|
+
}
|
|
1284
|
+
features[id] = true;
|
|
1285
|
+
}
|
|
1286
|
+
request.features = features;
|
|
616
1287
|
} else if (flag === "--scope") {
|
|
617
1288
|
if (value !== "user" && value !== "project") return { ok: false, error: `--scope must be user or project, got "${value}"` };
|
|
618
1289
|
request.scope = value;
|
|
@@ -622,30 +1293,35 @@ function parseFlags(argv: string[]): ParsedFlags {
|
|
|
622
1293
|
else return { ok: false, error: `unknown flag "${flag}"\n\n${CREATE_AGENT_USAGE}` };
|
|
623
1294
|
}
|
|
624
1295
|
|
|
625
|
-
return { ok: true, request, json, bodyFile, listArchetypes, listBenchmarks, help, yes, noDiscover, explicitBenchmarks };
|
|
1296
|
+
return { ok: true, request, json, bodyFile, listArchetypes, listBenchmarks, listFeatures, help, yes, noDiscover, explicitBenchmarks };
|
|
626
1297
|
}
|
|
627
1298
|
|
|
628
1299
|
/** Parse the flag form of `/create-agent` into a request. */
|
|
629
1300
|
export function parseCreateAgentArgs(argv: string[]): ParsedCreateAgentArgs {
|
|
630
1301
|
const parsed = parseFlags(argv);
|
|
631
1302
|
if (!parsed.ok) return parsed;
|
|
632
|
-
if (!parsed.help && !parsed.listArchetypes && !parsed.listBenchmarks) {
|
|
1303
|
+
if (!parsed.help && !parsed.listArchetypes && !parsed.listBenchmarks && !parsed.listFeatures) {
|
|
633
1304
|
if (parsed.request.name === "") return { ok: false, error: `--name is required\n\n${CREATE_AGENT_USAGE}` };
|
|
634
1305
|
if (parsed.request.purpose === "") return { ok: false, error: `--purpose is required\n\n${CREATE_AGENT_USAGE}` };
|
|
635
1306
|
}
|
|
636
1307
|
return { ...parsed, freeText: false };
|
|
637
1308
|
}
|
|
638
1309
|
|
|
1310
|
+
/** Metrics every archetype already weights, so naming one in prose adds nothing
|
|
1311
|
+
* and must not be read as a benchmark. `reasoning` is deliberately absent: not
|
|
1312
|
+
* every archetype weights it, so it stays extractable. */
|
|
1313
|
+
const ALWAYS_WEIGHTED_METRICS: Record<string, true> = { general: true, price: true, throughput: true };
|
|
1314
|
+
|
|
639
1315
|
/**
|
|
640
1316
|
* Metric names a free-text request names explicitly. Whole-word, case-insensitive,
|
|
641
1317
|
* with `_`/`-`/space interchangeable inside a name (`long_context` ↔ "long context").
|
|
642
|
-
* `price`/`throughput`
|
|
643
|
-
*
|
|
1318
|
+
* The always-weighted backbone (`general`/`price`/`throughput`) is excluded: every
|
|
1319
|
+
* archetype already weights them, so ordinary prose naming one adds nothing.
|
|
644
1320
|
*/
|
|
645
1321
|
export function extractBenchmarks(text: string): string[] {
|
|
646
1322
|
const found: string[] = [];
|
|
647
1323
|
for (const metric of Object.keys(KNOWN_METRICS)) {
|
|
648
|
-
if (metric
|
|
1324
|
+
if (metric in ALWAYS_WEIGHTED_METRICS) continue;
|
|
649
1325
|
const pattern = metric.replace(/_/g, "[ _-]?");
|
|
650
1326
|
if (new RegExp(`\\b${pattern}\\b`, "i").test(text)) found.push(metric);
|
|
651
1327
|
}
|
|
@@ -689,7 +1365,7 @@ export function parseCreateAgentInput(raw: string): ParsedCreateAgentArgs {
|
|
|
689
1365
|
if (purpose === "") return { ok: false, error: `nothing to create\n\n${CREATE_AGENT_USAGE}` };
|
|
690
1366
|
const parsed = parseFlags(firstFlag === -1 ? [] : tokens.slice(firstFlag));
|
|
691
1367
|
if (!parsed.ok) return parsed;
|
|
692
|
-
if (parsed.help || parsed.listArchetypes || parsed.listBenchmarks) return { ...parsed, freeText: false };
|
|
1368
|
+
if (parsed.help || parsed.listArchetypes || parsed.listBenchmarks || parsed.listFeatures) return { ...parsed, freeText: false };
|
|
693
1369
|
|
|
694
1370
|
parsed.request.purpose = purpose;
|
|
695
1371
|
if (parsed.request.extraBenchmarks === undefined) {
|
|
@@ -710,10 +1386,48 @@ export function formatArchetypes(): string {
|
|
|
710
1386
|
}).join("\n\n");
|
|
711
1387
|
}
|
|
712
1388
|
|
|
1389
|
+
/** One focus-metric line: coverage plus the eight new signals. */
|
|
1390
|
+
function assessmentLine(metric: string, a: FocusMetricAssessment): string {
|
|
1391
|
+
const c = a.coverage;
|
|
1392
|
+
const head =
|
|
1393
|
+
c.status === "unknown"
|
|
1394
|
+
? `${metric} unknown (field size unavailable)`
|
|
1395
|
+
: `${metric} ${c.covered}/${c.total} (${c.share === null ? "?" : `${(c.share * 100).toFixed(1)}%`}) ${c.status}`;
|
|
1396
|
+
const dispersion = a.dispersion.status === "unknown" ? "unknown" : `${a.dispersion.value?.toFixed(3)} ${a.dispersion.status}`;
|
|
1397
|
+
const composition =
|
|
1398
|
+
a.composition.status === "unknown" ? "unknown" : a.composition.status === "ok" ? "ok" : `omits ${a.composition.omittedOrgs.join(", ")}`;
|
|
1399
|
+
const freshness = a.freshness.status === "unknown" ? "unknown" : `${a.freshness.monthsBehind?.toFixed(1)}mo ${a.freshness.status}`;
|
|
1400
|
+
const trust =
|
|
1401
|
+
a.trust.status === "unknown"
|
|
1402
|
+
? a.trust.community
|
|
1403
|
+
? "community"
|
|
1404
|
+
: "unknown"
|
|
1405
|
+
: `${a.trust.selfReported}/${a.trust.covered} ${a.trust.status}${a.trust.community ? " community" : ""}`;
|
|
1406
|
+
const modality =
|
|
1407
|
+
a.modality.status === "unknown"
|
|
1408
|
+
? "unknown"
|
|
1409
|
+
: `${a.modality.value ?? "?"}${a.modality.multimodalShare === null ? "" : ` ${(a.modality.multimodalShare * 100).toFixed(0)}% multimodal`}`;
|
|
1410
|
+
const maintenance = a.maintenance.status === "unknown" ? "unknown" : `${a.maintenance.monthsOld?.toFixed(1)}mo ${a.maintenance.status}`;
|
|
1411
|
+
const provenance =
|
|
1412
|
+
a.provenance.status === "unknown"
|
|
1413
|
+
? "unknown"
|
|
1414
|
+
: `${a.provenance.owner ?? "?"}${a.provenance.dominantShare === null ? "" : ` ${(a.provenance.dominantShare * 100).toFixed(0)}% ${a.provenance.dominantOrg ?? "?"}`} ${a.provenance.compositionAgreement}`;
|
|
1415
|
+
const crossSource =
|
|
1416
|
+
a.crossSource.status === "unknown"
|
|
1417
|
+
? "unknown"
|
|
1418
|
+
: `${a.crossSource.compared} priceΔ${a.crossSource.priceDivergence === null ? "?" : `${(a.crossSource.priceDivergence * 100).toFixed(0)}%`} ctxΔ${a.crossSource.contextDivergence === null ? "?" : `${(a.crossSource.contextDivergence * 100).toFixed(0)}%`} ${a.crossSource.status}`;
|
|
1419
|
+
return `${head} dispersion ${dispersion} composition ${composition} freshness ${freshness} trust ${trust} modality ${modality} maintenance ${maintenance} provenance ${provenance} cross-source ${crossSource}`;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
713
1422
|
/** The human-readable report for a completed (or dry-run) create. */
|
|
714
|
-
export function formatCreateAgentReport(
|
|
1423
|
+
export function formatCreateAgentReport(
|
|
1424
|
+
result: CreatedAgent,
|
|
1425
|
+
updaterHint: string,
|
|
1426
|
+
opts: { differentiation?: string | null } = {},
|
|
1427
|
+
): string {
|
|
715
1428
|
const weights = Object.entries(result.def.weights).map(([metric, weight]) => `${metric}=${weight}`).join(",");
|
|
716
1429
|
const fit = result.matched.length > 0 ? `matched ${result.matched.join(", ")}` : "fallback (no purpose keyword matched)";
|
|
1430
|
+
const featureEntries = Object.entries(result.def.features ?? {});
|
|
717
1431
|
const lines = [
|
|
718
1432
|
`create-agent: ${result.dryRun ? "(dry run) " : ""}${result.name}`,
|
|
719
1433
|
` archetype: ${result.archetype.id} (${result.archetype.label}) — ${fit}`,
|
|
@@ -722,9 +1436,55 @@ export function formatCreateAgentReport(result: CreatedAgent, updaterHint: strin
|
|
|
722
1436
|
` agent: ${result.agentPath}${result.readOnly ? " (read-only)" : ""}`,
|
|
723
1437
|
` model: ${result.model}`,
|
|
724
1438
|
];
|
|
1439
|
+
if (featureEntries.length > 0) lines.push(` features: ${featureEntries.map(([id, on]) => `${id}=${on}`).join(",")}`);
|
|
725
1440
|
if (result.bench.added.length > 0) lines.push(` benchmarks: added ${result.bench.added.join(", ")}`);
|
|
726
1441
|
if (result.bench.duplicates.length > 0) lines.push(` benchmarks: already weighted (skipped) ${result.bench.duplicates.join(", ")}`);
|
|
727
1442
|
if (result.bench.unknown.length > 0) lines.push(` benchmarks: unknown (skipped) ${result.bench.unknown.join(", ")}`);
|
|
1443
|
+
if (result.bench.dropped.length > 0) lines.push(` benchmarks: dropped (focus cap ${FOCUS_METRIC_CAP}) ${result.bench.dropped.join(", ")}`);
|
|
1444
|
+
for (const [metric, a] of Object.entries(result.bench.assessments)) lines.push(` coverage: ${assessmentLine(metric, a)}`);
|
|
1445
|
+
for (const [metric, a] of Object.entries(result.bench.assessments)) {
|
|
1446
|
+
if (a.coverage.status === "below-bar") {
|
|
1447
|
+
lines.push(
|
|
1448
|
+
` warning: ${metric} coverage ${a.coverage.covered}/${a.coverage.total} is below the ${Math.round(FOCUS_COVERAGE_FLOOR * 100)}% bar — the role may rank on coverage, not quality`,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (a.dispersion.status === "below-bar") {
|
|
1452
|
+
lines.push(` warning: ${metric} dispersion ${a.dispersion.value?.toFixed(3)} is below the ${FOCUS_DISPERSION_FLOOR} bar — the metric cannot separate models`);
|
|
1453
|
+
}
|
|
1454
|
+
if (a.composition.status === "below-bar") {
|
|
1455
|
+
lines.push(` warning: ${metric} omits ${a.composition.omittedOrgs.join(", ")} — the source is provider-skewed`);
|
|
1456
|
+
}
|
|
1457
|
+
if (a.freshness.status === "below-bar") {
|
|
1458
|
+
lines.push(` warning: ${metric} trails the pool by ${a.freshness.monthsBehind?.toFixed(1)} months — the source is stale`);
|
|
1459
|
+
}
|
|
1460
|
+
if (a.trust.status === "below-bar") {
|
|
1461
|
+
lines.push(
|
|
1462
|
+
` warning: ${metric} trust ${a.trust.selfReported}/${a.trust.covered} self-reported is above the ${FOCUS_SELF_REPORTED_MAX_SHARE} bar — the source is mostly vendor-submitted`,
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
if (a.maintenance.status === "below-bar") {
|
|
1466
|
+
lines.push(
|
|
1467
|
+
` warning: ${metric} dataset last updated ${a.maintenance.monthsOld?.toFixed(1)} months ago — the benchmark may be unmaintained`,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
if (a.provenance.status === "below-bar") {
|
|
1471
|
+
const share = a.provenance.dominantShare === null ? "?" : `${(a.provenance.dominantShare * 100).toFixed(0)}%`;
|
|
1472
|
+
lines.push(
|
|
1473
|
+
` warning: ${metric} provenance ${a.provenance.dominantOrg ?? "?"} dominates ${share} of the source's entries — the benchmark is vendor-populated`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
if (a.provenance.compositionAgreement === "disagree") {
|
|
1477
|
+
lines.push(
|
|
1478
|
+
` warning: ${metric} provenance disagrees with the composition axis — the source's own org mix and the pool-derived check do not concur`,
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (a.crossSource.status === "below-bar") {
|
|
1482
|
+
lines.push(
|
|
1483
|
+
` warning: ${metric} cross-source price/context diverges from the source's own per-entry values — the price basis may be stale or mis-joined`,
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (opts.differentiation) lines.push(` warning: ${opts.differentiation}`);
|
|
728
1488
|
if (result.backupPath !== null) lines.push(` backup: ${result.backupPath}`);
|
|
729
1489
|
if (result.dryRun) lines.push(" nothing written (--dry-run)");
|
|
730
1490
|
else lines.push(` next: ${updaterHint}`);
|