@suluk/agents 0.1.0 → 0.1.1

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.1",
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 }
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,27 @@ 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
+ // GOVERNANCE GATE (mechanical): a governed skill MUST pin — router/latest are non-reproducible + cannot bind an endpoint.
96
+ if (isGoverned(doc, agentName) && mode !== "pinned")
97
+ 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".`);
98
+
99
+ let target: ResolvedTarget;
100
+ let pickPinned: boolean;
101
+ if (mode === "router") { target = { kind: "router", model: "openrouter/auto", allowedModels: ids, costQualityTradeoff: deriveCQT(skill) }; pickPinned = false; }
102
+ 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; }
103
+ else { target = { kind: "pinned", model: ids[0] ?? "" }; pickPinned = true; }
104
+
105
+ return { ids, from, selection, snapshotHash, target, pickPinned };
54
106
  }
@@ -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,45 @@ 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
+ });