@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/agents",
3
- "version": "0.1.0",
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.7",
23
- "@suluk/models": "^0.1.0"
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 (C027 contentHash discipline). */
56
- modelSelection?: { skill: string; ids: string[]; from: "declared" | "selected"; snapshotHash: string | null }[];
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 } : {}) };
@@ -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 pick was made against — reproducibility (null when declared). */
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
- if (skill?.model?.length && !skill.modelProfile && !skill.modelPrefer) return { ids: skill.model, from: "declared", snapshotHash: null };
52
- const selection = resolveSkillModels(doc, agentName, skillName, catalog, minWindowRequired);
53
- return { ids: selection.ranked.map((r) => r.id), from: "selected", selection, snapshotHash: catalog.snapshotHash };
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
+ });