@suluk/agents 0.1.0 → 0.1.2
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 +3 -3
- package/src/index.ts +1 -1
- package/src/manifest.ts +4 -3
- package/src/model-select.ts +69 -7
- package/test/model-select.test.ts +64 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Suluk Agent composition (C027): lint + project an `x-suluk-agents` map (skills + deterministic routes + by-name sub-agents) to a Claude plugin AND an OpenRouter/OpenAI-compatible manifest — one contract, two artifacts, zero network at generate time. Determinism is DECLARED not enforced; the matcher never reads an agent field. CANDIDATE tooling — NOT official OAS.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
".": "./src/index.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@suluk/core": "^0.1.
|
|
23
|
-
"@suluk/models": "^0.1.
|
|
22
|
+
"@suluk/core": "^0.1.9",
|
|
23
|
+
"@suluk/models": "^0.1.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/index.ts
CHANGED
|
@@ -40,5 +40,5 @@ export {
|
|
|
40
40
|
} from "./context";
|
|
41
41
|
// model-selection seam (C027 × @suluk/models): a skill declares NEEDS (profile + the analyzer's minWindowRequired +
|
|
42
42
|
// the C028 allowlist MEET) and the catalog picks the best CURRENT model — never a hard-coded id.
|
|
43
|
-
export { resolveSkillModels, skillModels, type SkillModelResolution } from "./model-select";
|
|
43
|
+
export { resolveSkillModels, skillModels, deriveCQT, type SkillModelResolution, type ResolvedTarget } from "./model-select";
|
|
44
44
|
export { selectModel, deriveRequirements, SEED_CATALOG, PROFILES, type ModelCatalog, type SelectResult, type Preferences, type HardFilters } from "@suluk/models";
|
package/src/manifest.ts
CHANGED
|
@@ -52,8 +52,9 @@ export interface AgentManifestNode {
|
|
|
52
52
|
/** operator-effective surface after x-suluk-policy (C028) — so the C021 signature covers the operator's caps. */
|
|
53
53
|
governed?: AgentManifestGoverned;
|
|
54
54
|
/** catalog-pinned model selection per skill (present only when agentManifest is given a catalog) — reproducible: the
|
|
55
|
-
* snapshotHash is signed, so a re-pick week-over-week with no author edit is auditable
|
|
56
|
-
|
|
55
|
+
* snapshotHash is signed (the SURVIVOR SET), so a re-pick week-over-week with no author edit is auditable. `resolve`
|
|
56
|
+
* is the C030 mode; `pickPinned` false ⇒ set-pinned but the served id is NOT reproducible (router/latest). */
|
|
57
|
+
modelSelection?: { skill: string; ids: string[]; from: "declared" | "selected"; snapshotHash: string | null; resolve: "pinned" | "router" | "latest"; pickPinned: boolean }[];
|
|
57
58
|
}
|
|
58
59
|
export interface AgentManifest {
|
|
59
60
|
manifestVersion: 1;
|
|
@@ -110,7 +111,7 @@ export function agentManifest(doc: OpenAPIv4Document, agentName: string, opts: {
|
|
|
110
111
|
if (opts.catalog) {
|
|
111
112
|
modelSelection = Object.keys(a.skills ?? {}).sort().map((sk) => {
|
|
112
113
|
const r = skillModels(doc, key, sk, opts.catalog!, minWinByAgent[key]);
|
|
113
|
-
return { skill: sk, ids: r.ids, from: r.from, snapshotHash: r.snapshotHash };
|
|
114
|
+
return { skill: sk, ids: r.ids, from: r.from, snapshotHash: r.snapshotHash, resolve: r.target.kind, pickPinned: r.pickPinned };
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
return { name: key, description: a.description, effectiveScope: effective[key] ?? null, skills, routes, subAgents, ...(governed ? { governed } : {}), ...(modelSelection ? { modelSelection } : {}) };
|
package/src/model-select.ts
CHANGED
|
@@ -6,18 +6,54 @@
|
|
|
6
6
|
* from the skill's `modelProfile`/`modelPrefer`. An explicit `model[]` (with no profile/prefer) is the author's
|
|
7
7
|
* opt-out — returned verbatim.
|
|
8
8
|
*/
|
|
9
|
-
import type { OpenAPIv4Document } from "@suluk/core";
|
|
10
|
-
import { selectModel, deriveRequirements, type ModelCatalog, type SelectResult, type Preferences } from "@suluk/models";
|
|
9
|
+
import type { OpenAPIv4Document, SulukSkillRef } from "@suluk/core";
|
|
10
|
+
import { selectModel, deriveRequirements, PROFILES, type ModelCatalog, type SelectResult, type Preferences } from "@suluk/models";
|
|
11
11
|
import { agentMap } from "./resolve";
|
|
12
12
|
import { policiesFor } from "./policy";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* How a skill RESOLVES to a runtime model (C030, council wf_75f87ab6-b1b — unanimous hybrid). We keep the survivor
|
|
16
|
+
* SET (governance + caps + min-context, the moat) and either PIN a concrete reproducible id, or DELEGATE the
|
|
17
|
+
* per-request pick to OpenRouter's auto-router fenced by our ENUMERATED survivor allowlist (never a wildcard).
|
|
18
|
+
*/
|
|
19
|
+
export type ResolvedTarget =
|
|
20
|
+
| { kind: "pinned"; model: string }
|
|
21
|
+
| { kind: "router"; model: "openrouter/auto"; allowedModels: string[]; costQualityTradeoff: number; provider?: { zdr: true } }
|
|
22
|
+
| { kind: "latest"; model: string; note: string };
|
|
23
|
+
|
|
14
24
|
export interface SkillModelResolution {
|
|
15
25
|
ids: string[];
|
|
16
26
|
from: "declared" | "selected";
|
|
17
27
|
/** the selector result (filter trace + per-axis why + coverage gaps) when `from === "selected"`. */
|
|
18
28
|
selection?: SelectResult;
|
|
19
|
-
/** the catalog snapshot the
|
|
29
|
+
/** the catalog snapshot the SURVIVOR SET was pinned against (null when declared). */
|
|
20
30
|
snapshotHash: string | null;
|
|
31
|
+
/** the resolved runtime target (pin / router / latest). */
|
|
32
|
+
target: ResolvedTarget;
|
|
33
|
+
/** true ⇒ the SERVED model id is reproducible (pinned). false ⇒ set-pinned but pick-NOT-pinned (router/latest). */
|
|
34
|
+
pickPinned: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** An operator policy governs this agent ⇒ FORCE PIN (reproducible + auditable; the runtime router cannot bind an
|
|
38
|
+
* endpoint region/retention, and its pick is non-reproducible across dates). C030 governance gate — mechanical. */
|
|
39
|
+
function isGoverned(doc: OpenAPIv4Document, agentName: string): boolean {
|
|
40
|
+
return policiesFor(doc, agentName).length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** cost_quality_tradeoff 0..10 (0=quality, 10=cost) — mechanical from the profile's cost-vs-intelligence weights
|
|
44
|
+
* (set explicitly; do NOT inherit OpenRouter's cost-leaning default of 7). */
|
|
45
|
+
export function deriveCQT(skill: SulukSkillRef | undefined): number {
|
|
46
|
+
const base = skill?.modelProfile ? PROFILES[skill.modelProfile].prefer : { intelligence: 2, cost: 2, speed: 1, context: 1 };
|
|
47
|
+
const w = { ...base, ...(skill?.modelPrefer ?? {}) };
|
|
48
|
+
const denom = (w.cost ?? 0) + (w.intelligence ?? 0);
|
|
49
|
+
return denom === 0 ? 5 : Math.max(0, Math.min(10, Math.round((10 * (w.cost ?? 0)) / denom)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Best-effort `~author/family-latest` alias for the latest-resolution opt-in (defers the concrete version). */
|
|
53
|
+
function toLatestAlias(id: string): string {
|
|
54
|
+
const [author, rest] = id.split("/");
|
|
55
|
+
const family = (rest ?? "").replace(/[-.:@](\d.*|latest|preview|chat|instruct).*$/i, "");
|
|
56
|
+
return author && family ? `~${author}/${family}-latest` : id;
|
|
21
57
|
}
|
|
22
58
|
|
|
23
59
|
/** The C028 modelAllowlist MEET across every policy governing this agent (intersection of the present allowlists). */
|
|
@@ -44,11 +80,37 @@ export function resolveSkillModels(doc: OpenAPIv4Document, agentName: string, sk
|
|
|
44
80
|
return selectModel(reqs, prefs, catalog);
|
|
45
81
|
}
|
|
46
82
|
|
|
47
|
-
/** The public seam: the models for a skill — its DECLARED list (opt-out) or the catalog-SELECTED ranked ids
|
|
83
|
+
/** The public seam: the models for a skill — its DECLARED list (opt-out) or the catalog-SELECTED ranked ids, resolved
|
|
84
|
+
* to a runtime TARGET (pin / router / latest) under the C030 governance gate. */
|
|
48
85
|
export function skillModels(doc: OpenAPIv4Document, agentName: string, skillName: string, catalog: ModelCatalog, minWindowRequired?: number): SkillModelResolution {
|
|
49
86
|
const skill = agentMap(doc)[agentName]?.skills?.[skillName];
|
|
50
87
|
// explicit model[] with no profile/prefer ⇒ the author opted out of catalog selection
|
|
51
|
-
|
|
52
|
-
const selection = resolveSkillModels(doc, agentName, skillName, catalog, minWindowRequired);
|
|
53
|
-
|
|
88
|
+
const declared = !!(skill?.model?.length && !skill?.modelProfile && !skill?.modelPrefer);
|
|
89
|
+
const selection = declared ? undefined : resolveSkillModels(doc, agentName, skillName, catalog, minWindowRequired);
|
|
90
|
+
const ids = declared ? skill!.model! : selection!.ranked.map((r) => r.id);
|
|
91
|
+
const from: "declared" | "selected" = declared ? "declared" : "selected";
|
|
92
|
+
const snapshotHash = declared ? null : catalog.snapshotHash;
|
|
93
|
+
|
|
94
|
+
const mode = skill?.modelResolve ?? "pinned";
|
|
95
|
+
const governed = isGoverned(doc, agentName);
|
|
96
|
+
// ZDR (C030, verified 2026-06-13 — `provider:{zdr:true}` combines with `model:"openrouter/auto"`, 200 live): we have no
|
|
97
|
+
// per-model ZDR FACT to pin against, so ZDR is enforceable ONLY at runtime via the router → a `zdr` skill resolves to the
|
|
98
|
+
// ROUTER regardless of `modelResolve`. Authors who pinned get it: the pin can't honor ZDR, the router can.
|
|
99
|
+
const wantsZdr = !!skill?.modelRequire?.zdr;
|
|
100
|
+
// GOVERNANCE GATE (mechanical): a governed skill MUST pin — router/latest are non-reproducible + cannot bind an endpoint.
|
|
101
|
+
if (governed && mode !== "pinned")
|
|
102
|
+
throw new Error(`@suluk/agents: skill "${skillName}" of agent "${agentName}" is GOVERNED by an operator policy — modelResolve:"${mode}" is inadmissible (a governed skill must be "pinned" for reproducible, auditable, endpoint-bindable selection). Remove the policy or use "pinned".`);
|
|
103
|
+
// ZDR-under-governance is unsatisfiable via OpenRouter: ZDR needs the router (`provider.zdr`), governance forces a pin, and
|
|
104
|
+
// region/license have NO endpoint knob — fail loud rather than silently dropping the ZDR constraint or the governance pin.
|
|
105
|
+
if (governed && wantsZdr)
|
|
106
|
+
throw new Error(`@suluk/agents: skill "${skillName}" of agent "${agentName}" requires ZDR (modelRequire.zdr) AND is GOVERNED by an operator policy — unsatisfiable: ZDR is enforced only via the router's provider.zdr, but a governed skill must pin (region/license have no OpenRouter endpoint knob). Drop one.`);
|
|
107
|
+
|
|
108
|
+
let target: ResolvedTarget;
|
|
109
|
+
let pickPinned: boolean;
|
|
110
|
+
if (wantsZdr) { target = { kind: "router", model: "openrouter/auto", allowedModels: ids, costQualityTradeoff: deriveCQT(skill), provider: { zdr: true } }; pickPinned = false; }
|
|
111
|
+
else if (mode === "router") { target = { kind: "router", model: "openrouter/auto", allowedModels: ids, costQualityTradeoff: deriveCQT(skill) }; pickPinned = false; }
|
|
112
|
+
else if (mode === "latest") { target = { kind: "latest", model: toLatestAlias(ids[0] ?? ""), note: "~-latest defers the concrete version to request time — NOT reproducible (recorded in the why-explainer)" }; pickPinned = false; }
|
|
113
|
+
else { target = { kind: "pinned", model: ids[0] ?? "" }; pickPinned = true; }
|
|
114
|
+
|
|
115
|
+
return { ids, from, selection, snapshotHash, target, pickPinned };
|
|
54
116
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, expect, describe } from "bun:test";
|
|
2
2
|
import type { OpenAPIv4Document } from "@suluk/core";
|
|
3
|
-
import { skillModels, resolveSkillModels, SEED_CATALOG } from "../src/index";
|
|
3
|
+
import { skillModels, resolveSkillModels, deriveCQT, SEED_CATALOG } from "../src/index";
|
|
4
4
|
|
|
5
5
|
/** An agent with routes (⇒ needs tool-calling) and two skills: a needs-based one + an explicit opt-out. */
|
|
6
6
|
function doc(): OpenAPIv4Document {
|
|
@@ -54,3 +54,66 @@ describe("C027 × @suluk/models — the model-selection seam", () => {
|
|
|
54
54
|
expect(r.ids).toEqual(["google/gemini-2.5-flash"]); // even though cheap-fast might prefer gpt-4o-mini
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
describe("C030 resolution target — pin (default) / router (delegate) / latest, governance-gated", () => {
|
|
59
|
+
test("default is a REPRODUCIBLE pin", () => {
|
|
60
|
+
const r = skillModels(doc(), "conin", "operate", SEED_CATALOG);
|
|
61
|
+
expect(r.target).toEqual({ kind: "pinned", model: r.ids[0] });
|
|
62
|
+
expect(r.pickPinned).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("modelResolve:'router' (ungoverned) delegates to openrouter/auto with an ENUMERATED survivor allowlist", () => {
|
|
66
|
+
const d = doc();
|
|
67
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "cheap-fast", modelResolve: "router" };
|
|
68
|
+
const r = skillModels(d, "conin", "operate", SEED_CATALOG);
|
|
69
|
+
expect(r.target.kind).toBe("router");
|
|
70
|
+
if (r.target.kind === "router") {
|
|
71
|
+
expect(r.target.model).toBe("openrouter/auto");
|
|
72
|
+
expect(r.target.allowedModels).toEqual(r.ids); // enumerated survivor ids — NEVER a wildcard
|
|
73
|
+
expect(r.target.costQualityTradeoff).toBeGreaterThan(5); // cheap-fast leans cost
|
|
74
|
+
}
|
|
75
|
+
expect(r.pickPinned).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a GOVERNED skill declaring 'router' FAILS LOUD (must pin — reproducible + endpoint-bindable)", () => {
|
|
79
|
+
const d = doc();
|
|
80
|
+
d["x-suluk-policy"] = { fleet: { appliesTo: ["#/x-suluk-agents/conin"], modelAllowlist: ["google/gemini-2.5-flash"] } };
|
|
81
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "balanced", modelResolve: "router" };
|
|
82
|
+
expect(() => skillModels(d, "conin", "operate", SEED_CATALOG)).toThrow(/GOVERNED|pinned/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("modelResolve:'latest' emits a ~-latest alias (non-reproducible, recorded)", () => {
|
|
86
|
+
const d = doc();
|
|
87
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "max-reasoning", modelResolve: "latest" };
|
|
88
|
+
const r = skillModels(d, "conin", "operate", SEED_CATALOG);
|
|
89
|
+
expect(r.target.kind).toBe("latest");
|
|
90
|
+
if (r.target.kind === "latest") expect(r.target.model.startsWith("~")).toBe(true);
|
|
91
|
+
expect(r.pickPinned).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("deriveCQT is mechanical: cheap-fast leans cost (>5), max-reasoning leans quality (0)", () => {
|
|
95
|
+
expect(deriveCQT({ modelProfile: "cheap-fast" })).toBeGreaterThan(5);
|
|
96
|
+
expect(deriveCQT({ modelProfile: "max-reasoning" })).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("C030 ZDR — verified 2026-06-13 (provider.zdr + openrouter/auto combine, 200 live)", () => {
|
|
101
|
+
test("a modelRequire.zdr skill resolves to the ROUTER with provider:{zdr:true} EVEN at the default (pinned) mode — no per-model ZDR fact to pin against", () => {
|
|
102
|
+
const d = doc();
|
|
103
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "balanced", modelRequire: { zdr: true } }; // no modelResolve ⇒ default pinned
|
|
104
|
+
const r = skillModels(d, "conin", "operate", SEED_CATALOG);
|
|
105
|
+
expect(r.target.kind).toBe("router");
|
|
106
|
+
if (r.target.kind === "router") {
|
|
107
|
+
expect(r.target.provider).toEqual({ zdr: true });
|
|
108
|
+
expect(r.target.allowedModels).toEqual(r.ids); // ZDR is enforced at the endpoint; the fence still enumerates survivors
|
|
109
|
+
}
|
|
110
|
+
expect(r.pickPinned).toBe(false); // the served id is logged-not-pinned (ZDR endpoint chosen at runtime)
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("ZDR + an operator policy is UNSATISFIABLE via OpenRouter — fails loud (ZDR needs the router, governance forces a pin)", () => {
|
|
114
|
+
const d = doc();
|
|
115
|
+
d["x-suluk-policy"] = { fleet: { appliesTo: ["#/x-suluk-agents/conin"], modelAllowlist: ["google/gemini-2.5-flash"] } };
|
|
116
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "balanced", modelRequire: { zdr: true } };
|
|
117
|
+
expect(() => skillModels(d, "conin", "operate", SEED_CATALOG)).toThrow(/ZDR|unsatisfiable/i);
|
|
118
|
+
});
|
|
119
|
+
});
|