@suluk/cockpit 0.1.15 → 0.1.17
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 +4 -3
- package/src/agents.ts +191 -0
- package/src/index.ts +6 -0
- package/test/agents.test.ts +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/cockpit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "The pure cockpit core (cycle model · builder model · codegen · deploy planning · validate/audit/preview) shared by the vscode extension and the /superadmin web admin panel. CANDIDATE tooling.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
".": "./src/index.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@suluk/core": "^0.1.
|
|
22
|
+
"@suluk/core": "^0.1.9",
|
|
23
23
|
"@suluk/hono": "^0.1.2",
|
|
24
24
|
"@suluk/scalar": "^0.1.2",
|
|
25
25
|
"@suluk/swagger": "^0.1.2",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"@suluk/builder": "^0.1.11",
|
|
28
28
|
"@suluk/deploy": "^0.1.3",
|
|
29
29
|
"@suluk/cost": "^0.1.2",
|
|
30
|
-
"@suluk/visual": "^0.1.3"
|
|
30
|
+
"@suluk/visual": "^0.1.3",
|
|
31
|
+
"@suluk/agents": "^0.1.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/bun": "latest"
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AGENTS view (C027) — the cockpit's OBSERVE surface for the `x-suluk-agents` composition layer. Pure
|
|
3
|
+
* (no host) → unit-tested; the extension/admin shells render it. STRICTLY OBSERVE: this derives the static tier
|
|
4
|
+
* tree, effective (intersection) scope, the gate findings, the worst-case reachable surface, and a PROJECTION
|
|
5
|
+
* PREVIEW (artifact file/tool NAMES only). It NEVER executes an agent, fetches a preprompt, or touches a
|
|
6
|
+
* credential — agent execution + secrets live outside the cockpit (C020 no-credentials seam / C023 L3 line).
|
|
7
|
+
*/
|
|
8
|
+
import type { OpenAPIv4Document } from "@suluk/core";
|
|
9
|
+
import {
|
|
10
|
+
lintAgents, lintOk, reachableSurface, analyzeScopes, resolveOperationRef,
|
|
11
|
+
agentMap, subAgentKey, effectiveUnderPolicies, policiesFor, lintPolicy, contextReport, skillModels,
|
|
12
|
+
type LintFinding, type Scope, type AgentContextLoad, type UnflattenSuggestion, type FlattenSuggestion, type ModelCatalog,
|
|
13
|
+
} from "@suluk/agents";
|
|
14
|
+
|
|
15
|
+
export interface AgentSkillView {
|
|
16
|
+
name: string;
|
|
17
|
+
model: string[];
|
|
18
|
+
tier?: "resident" | "cold-tail";
|
|
19
|
+
/** has a provenance.contentHash ⇒ drift is detectable (the staleness binding). */
|
|
20
|
+
pinned: boolean;
|
|
21
|
+
source?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AgentRouteView {
|
|
24
|
+
name: string;
|
|
25
|
+
operationRef: string;
|
|
26
|
+
guarantee?: string;
|
|
27
|
+
/** serving partition: resident (default tool list) vs cold-tail (behind discover_tools). Absent ⇒ resident. */
|
|
28
|
+
tier?: "resident" | "cold-tail";
|
|
29
|
+
/** does the operationRef resolve to a real operation? (false ⇒ a dangling ref, like Conin's MCP-only primitive). */
|
|
30
|
+
resolves: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface AgentNodeView {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
/** an orchestrator has sub-agents; a leaf does not (the recursion base case). */
|
|
36
|
+
kind: "orchestrator" | "leaf";
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
/** scope after INTERSECTION along the reaching path (null = unconstrained). */
|
|
39
|
+
effectiveScope: Scope;
|
|
40
|
+
skills: AgentSkillView[];
|
|
41
|
+
routes: AgentRouteView[];
|
|
42
|
+
subAgents: string[];
|
|
43
|
+
/** worst-case statically-enumerable reach (tools + transitively-reachable sub-agents). */
|
|
44
|
+
reachable: { tools: string[]; agents: string[] };
|
|
45
|
+
/** OBSERVE-only preview of what projection WOULD emit — names, never executed, never credentialed. */
|
|
46
|
+
projection: { pluginFiles: string[]; openRouterTools: string[]; residentTools: string[]; discoverableTools: string[] };
|
|
47
|
+
/** operator governance diff (C028) — present only when an x-suluk-policy governs this agent. */
|
|
48
|
+
governed?: AgentGovernedView;
|
|
49
|
+
/** estimated default context load (resident instructions+tools+overhead) vs budget/window — the unflatten check (C027). */
|
|
50
|
+
context: AgentContextLoad;
|
|
51
|
+
/** per-skill model pick (C027 × @suluk/models) — present only when agentsView is given a catalog. OBSERVE-only:
|
|
52
|
+
* "why this model" (declared vs selected, top ids, deciding preference, UNKNOWN-coverage gaps). Never executes. */
|
|
53
|
+
modelSelection?: { skill: string; from?: "declared" | "selected"; ids?: string[]; resolve?: "pinned" | "router" | "latest"; pickPinned?: boolean; decidingPreference?: string; coverageGaps?: string[]; error?: string }[];
|
|
54
|
+
}
|
|
55
|
+
/** The agent-declared vs operator-effective diff + the cost three-number (cap / estimate / actual). Read-only. */
|
|
56
|
+
export interface AgentGovernedView {
|
|
57
|
+
effectiveScope: Scope;
|
|
58
|
+
effectiveMaxDepth?: number;
|
|
59
|
+
nestingForbidden: boolean;
|
|
60
|
+
deniedTools: string[];
|
|
61
|
+
deniedSubAgents: string[];
|
|
62
|
+
narrowings: { axis: string; detail: string }[];
|
|
63
|
+
/** the three distinct owners: cap (operator x-suluk-policy, enforced-by-adapter) / estimate (author) / actual (C026 runtime). */
|
|
64
|
+
cost: { cap: string | null; estimate: string | null; actual: string };
|
|
65
|
+
}
|
|
66
|
+
export interface AgentsView {
|
|
67
|
+
present: boolean;
|
|
68
|
+
agents: AgentNodeView[];
|
|
69
|
+
/** entry-point agents — not referenced as a sub-agent by any other agent. */
|
|
70
|
+
roots: string[];
|
|
71
|
+
findings: LintFinding[];
|
|
72
|
+
/** true ⇒ no error-severity findings across the whole map (the gate). */
|
|
73
|
+
installable: boolean;
|
|
74
|
+
/** context-budget findings (model-fit / over-budget / overloaded / empty-layer / passthrough / flattenable) — the right-sizing check. */
|
|
75
|
+
contextFindings: LintFinding[];
|
|
76
|
+
/** for every over-target agent: what to move to cold-tail or extract into a sub-agent (split DOWN). */
|
|
77
|
+
unflatten: UnflattenSuggestion[];
|
|
78
|
+
/** for every thin/redundant layer: what to collapse up (the dual — merge UP). */
|
|
79
|
+
flatten: FlattenSuggestion[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Agents referenced as a sub-agent by someone else (so the complement is the set of roots). */
|
|
83
|
+
function referencedChildren(doc: OpenAPIv4Document): Set<string> {
|
|
84
|
+
const ref = new Set<string>();
|
|
85
|
+
for (const a of Object.values(agentMap(doc))) {
|
|
86
|
+
for (const r of Object.values(a.agents ?? {})) {
|
|
87
|
+
const k = subAgentKey(r.ref);
|
|
88
|
+
if (k) ref.add(k);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return ref;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The operator-governance diff for one agent (declared vs effective + the cost three-number). */
|
|
95
|
+
function governedView(doc: OpenAPIv4Document, agentName: string): AgentGovernedView {
|
|
96
|
+
const { effective, narrowings } = effectiveUnderPolicies(doc, agentName);
|
|
97
|
+
const policy = policiesFor(doc, agentName).find((p) => p.costCeiling);
|
|
98
|
+
const cap = policy?.costCeiling ? `${policy.costCeiling.amount} ${policy.costCeiling.amountUnit} (enforcedBy ${policy.costCeiling.enforcedBy})` : null;
|
|
99
|
+
const cost = agentMap(doc)[agentName]["x-suluk-cost"] as { estimateMicroUsd?: number; amount?: number } | undefined;
|
|
100
|
+
const est = cost?.estimateMicroUsd ?? cost?.amount;
|
|
101
|
+
return {
|
|
102
|
+
effectiveScope: effective.scope,
|
|
103
|
+
...(effective.maxDepth !== undefined ? { effectiveMaxDepth: effective.maxDepth } : {}),
|
|
104
|
+
nestingForbidden: effective.nestingForbidden,
|
|
105
|
+
deniedTools: effective.deniedTools,
|
|
106
|
+
deniedSubAgents: effective.deniedSubAgents,
|
|
107
|
+
narrowings: narrowings.map((n) => ({ axis: n.axis, detail: n.detail })),
|
|
108
|
+
// three distinct owners — cap (operator, enforced-by-adapter) / estimate (author) / actual (runtime, C026)
|
|
109
|
+
cost: { cap, estimate: typeof est === "number" ? `${est} µ$` : null, actual: "reconciled at runtime (C026)" },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Build the OBSERVE view-model for the agent layer of a document. Never throws; tolerates non-installable agents. */
|
|
114
|
+
export function agentsView(doc: OpenAPIv4Document, opts: { catalog?: ModelCatalog } = {}): AgentsView {
|
|
115
|
+
const map = agentMap(doc);
|
|
116
|
+
const names = Object.keys(map).sort((a, b) => a.localeCompare(b));
|
|
117
|
+
const present = names.length > 0;
|
|
118
|
+
const findings = [...lintAgents(doc), ...lintPolicy(doc)];
|
|
119
|
+
const cr = contextReport(doc, opts.catalog ? { catalog: opts.catalog } : {}); // static load (no snapshots ⇒ a lower bound)
|
|
120
|
+
|
|
121
|
+
// effective scopes across the whole map: merge a walk from every root (covers every reachable node)
|
|
122
|
+
const referenced = referencedChildren(doc);
|
|
123
|
+
const roots = names.filter((n) => !referenced.has(n));
|
|
124
|
+
const effAll: Record<string, Scope> = {};
|
|
125
|
+
for (const r of roots) Object.assign(effAll, analyzeScopes(doc, r).effective);
|
|
126
|
+
|
|
127
|
+
const agents: AgentNodeView[] = names.map((name) => {
|
|
128
|
+
const a = map[name];
|
|
129
|
+
const subAgents = Object.values(a.agents ?? {}).map((r) => r.ref).sort();
|
|
130
|
+
const skills: AgentSkillView[] = Object.entries(a.skills ?? {}).map(([sk, s]) => ({
|
|
131
|
+
name: sk,
|
|
132
|
+
model: s.model ?? [],
|
|
133
|
+
...(s.tier ? { tier: s.tier } : {}),
|
|
134
|
+
pinned: !!s.provenance?.contentHash,
|
|
135
|
+
...(s.provenance?.source ? { source: s.provenance.source } : {}),
|
|
136
|
+
})).sort((x, y) => x.name.localeCompare(y.name));
|
|
137
|
+
const routes: AgentRouteView[] = Object.entries(a.routes ?? {}).map(([rk, r]) => ({
|
|
138
|
+
name: rk,
|
|
139
|
+
operationRef: r.operationRef,
|
|
140
|
+
...(r.guarantee ? { guarantee: r.guarantee } : {}),
|
|
141
|
+
...(r.tier ? { tier: r.tier } : {}),
|
|
142
|
+
resolves: !!resolveOperationRef(doc, r.operationRef),
|
|
143
|
+
})).sort((x, y) => x.name.localeCompare(y.name));
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
description: a.description,
|
|
147
|
+
kind: subAgents.length > 0 ? "orchestrator" : "leaf",
|
|
148
|
+
...(a.maxDepth !== undefined ? { maxDepth: a.maxDepth } : {}),
|
|
149
|
+
effectiveScope: effAll[name] ?? (a.scope ?? null),
|
|
150
|
+
skills,
|
|
151
|
+
routes,
|
|
152
|
+
subAgents,
|
|
153
|
+
reachable: reachableSurface(doc, name),
|
|
154
|
+
projection: {
|
|
155
|
+
pluginFiles: ["plugin.json", ".mcp.json", ...skills.map((s) => `skills/${s.name}/SKILL.md`)],
|
|
156
|
+
openRouterTools: routes.map((r) => r.name),
|
|
157
|
+
// the tier-trim: only resident tools are in the default surface; cold-tail sits behind discover_tools
|
|
158
|
+
residentTools: routes.filter((r) => r.tier !== "cold-tail").map((r) => r.name),
|
|
159
|
+
discoverableTools: routes.filter((r) => r.tier === "cold-tail").map((r) => r.name),
|
|
160
|
+
},
|
|
161
|
+
...(policiesFor(doc, name).length > 0 ? { governed: governedView(doc, name) } : {}),
|
|
162
|
+
context: cr.loads.find((l) => l.agent === name)!,
|
|
163
|
+
...(opts.catalog ? {
|
|
164
|
+
modelSelection: Object.keys(a.skills ?? {}).sort().map((sk) => {
|
|
165
|
+
const minWin = cr.loads.find((l) => l.agent === name)?.minWindowRequired;
|
|
166
|
+
try {
|
|
167
|
+
const r = skillModels(doc, name, sk, opts.catalog!, minWin);
|
|
168
|
+
return {
|
|
169
|
+
skill: sk, from: r.from, ids: r.ids.slice(0, 3), resolve: r.target.kind, pickPinned: r.pickPinned,
|
|
170
|
+
...(r.selection?.ranked[0] ? { decidingPreference: r.selection.ranked[0].why.decidingPreference } : {}),
|
|
171
|
+
...(r.selection ? { coverageGaps: r.selection.coverageGaps } : {}),
|
|
172
|
+
};
|
|
173
|
+
} catch (e) { return { skill: sk, error: e instanceof Error ? e.message : String(e) }; } // governed + router ⇒ fail-loud, surfaced
|
|
174
|
+
}),
|
|
175
|
+
} : {}),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return { present, agents, roots, findings, installable: lintOk(findings), contextFindings: cr.findings, unflatten: cr.suggestions, flatten: cr.flatten };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A one-line ship-readiness summary for the agent layer (mirrors the cockpit's other *Summary helpers). */
|
|
183
|
+
export function agentsSummary(view: AgentsView): string {
|
|
184
|
+
if (!view.present) return "no agents (x-suluk-agents absent)";
|
|
185
|
+
const errs = view.findings.filter((f) => f.severity === "error").length;
|
|
186
|
+
const warns = view.findings.filter((f) => f.severity === "warning").length;
|
|
187
|
+
const verdict = view.installable ? "✓ installable" : `✕ ${errs} blocking`;
|
|
188
|
+
const unflatten = view.unflatten.length ? `, ${view.unflatten.length} to unflatten` : "";
|
|
189
|
+
const flatten = view.flatten.length ? `, ${view.flatten.length} to flatten` : "";
|
|
190
|
+
return `${view.agents.length} agent(s), ${view.roots.length} root(s) — ${verdict}${warns ? `, ${warns} warning(s)` : ""}${unflatten}${flatten}`;
|
|
191
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,12 @@ export { componentReport, approveComponents, type ComponentReport } from "./visu
|
|
|
23
23
|
export { type Baseline, primitiveCss } from "@suluk/visual";
|
|
24
24
|
// lifecycle / ship-readiness (L3): the round-trip loop as one checklist — authored → coherent → confident → generated → deployed.
|
|
25
25
|
export { contractGates, shipSummary, type Gate, type GateStatus } from "./lifecycle";
|
|
26
|
+
// agents (C027, OBSERVE): the x-suluk-agents tier tree, effective scope, gate findings, reachable surface + a
|
|
27
|
+
// projection preview — read-only; agent execution + secrets live OUTSIDE the cockpit (C020 no-credentials seam).
|
|
28
|
+
export {
|
|
29
|
+
agentsView, agentsSummary,
|
|
30
|
+
type AgentsView, type AgentNodeView, type AgentSkillView, type AgentRouteView, type AgentGovernedView,
|
|
31
|
+
} from "./agents";
|
|
26
32
|
// cost formatting, re-exported so the extension shell can render a live /cost ledger without a direct @suluk/cost dep.
|
|
27
33
|
export { formatMicroUsd, summarize, type CostSummary } from "@suluk/cost";
|
|
28
34
|
// modules (C021): install a contract fragment into the hub doc — the cockpit then re-projects it for free.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import type { OpenAPIv4Document, HttpMethod, Request } from "@suluk/core";
|
|
3
|
+
import { agentsView, agentsSummary } from "../src/index";
|
|
4
|
+
import { SEED_CATALOG } from "@suluk/agents";
|
|
5
|
+
|
|
6
|
+
const req = (method: HttpMethod): Request => ({ method, responses: { ok: { status: 200 } } });
|
|
7
|
+
|
|
8
|
+
const doc: OpenAPIv4Document = {
|
|
9
|
+
openapi: "4.0.0-candidate",
|
|
10
|
+
info: { title: "agents-view", version: "0" },
|
|
11
|
+
paths: {
|
|
12
|
+
"v1/deliverables": { requests: { generateDeliverable: req("post") } },
|
|
13
|
+
"v1/library/search": { requests: { searchLibrary: req("get") } },
|
|
14
|
+
},
|
|
15
|
+
"x-suluk-agents": {
|
|
16
|
+
conin: {
|
|
17
|
+
description: "Orchestrator: docs → graded deliverables.",
|
|
18
|
+
scope: ["project:read", "library:read"],
|
|
19
|
+
maxDepth: 1,
|
|
20
|
+
skills: { operate: { model: ["anthropic/claude-opus-4"], tier: "cold-tail", provenance: { source: "https://x/v1/instructions", contentHash: "sha256-abc", version: "1" } } },
|
|
21
|
+
routes: { generate_deliverable: { operationRef: "#/paths/v1~1deliverables/requests/generateDeliverable", guarantee: "same-in-same-out" } },
|
|
22
|
+
agents: { retrieval: { ref: "#/x-suluk-agents/coninRetrieval" } },
|
|
23
|
+
},
|
|
24
|
+
coninRetrieval: {
|
|
25
|
+
description: "Untrusted retrieval leaf.",
|
|
26
|
+
scope: ["library:read"],
|
|
27
|
+
maxDepth: 0,
|
|
28
|
+
skills: { search: { model: ["google/gemini-2.5-flash"], tier: "resident" } }, // no provenance ⇒ unpinned
|
|
29
|
+
routes: { search_library: { operationRef: "#/paths/v1~1library~1search/requests/searchLibrary" } },
|
|
30
|
+
agents: {},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("C027 cockpit agents view (OBSERVE)", () => {
|
|
36
|
+
const v = agentsView(doc);
|
|
37
|
+
|
|
38
|
+
test("derives the tier tree + roots", () => {
|
|
39
|
+
expect(v.present).toBe(true);
|
|
40
|
+
expect(v.agents.map((a) => a.name)).toEqual(["conin", "coninRetrieval"]);
|
|
41
|
+
expect(v.roots).toEqual(["conin"]); // retrieval is referenced as a sub-agent → not a root
|
|
42
|
+
const [conin, retr] = v.agents;
|
|
43
|
+
expect(conin.kind).toBe("orchestrator");
|
|
44
|
+
expect(retr.kind).toBe("leaf");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("shows effective (intersection) scope + skill pinning + route resolution", () => {
|
|
48
|
+
const conin = v.agents.find((a) => a.name === "conin")!;
|
|
49
|
+
const retr = v.agents.find((a) => a.name === "coninRetrieval")!;
|
|
50
|
+
expect(conin.effectiveScope).toEqual(["project:read", "library:read"]);
|
|
51
|
+
expect(retr.effectiveScope).toEqual(["library:read"]);
|
|
52
|
+
expect(conin.skills[0].pinned).toBe(true); // has contentHash
|
|
53
|
+
expect(retr.skills[0].pinned).toBe(false); // no provenance
|
|
54
|
+
expect(conin.routes[0].resolves).toBe(true);
|
|
55
|
+
expect(conin.reachable.tools).toEqual(["generate_deliverable", "search_library"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("projection preview is names-only (no execution, no creds)", () => {
|
|
59
|
+
const conin = v.agents.find((a) => a.name === "conin")!;
|
|
60
|
+
expect(conin.projection.pluginFiles).toEqual(["plugin.json", ".mcp.json", "skills/operate/SKILL.md"]);
|
|
61
|
+
expect(conin.projection.openRouterTools).toEqual(["generate_deliverable"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("tier-trim is visible in the projection preview (resident vs discoverable)", () => {
|
|
65
|
+
const t = structuredClone(doc);
|
|
66
|
+
t["x-suluk-agents"]!.coninRetrieval.routes!.search_library.tier = "cold-tail";
|
|
67
|
+
const retr = agentsView(t).agents.find((a) => a.name === "coninRetrieval")!;
|
|
68
|
+
expect(retr.projection.discoverableTools).toEqual(["search_library"]);
|
|
69
|
+
expect(retr.projection.residentTools).not.toContain("search_library");
|
|
70
|
+
expect(retr.routes.find((r) => r.name === "search_library")!.tier).toBe("cold-tail");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("installable + summary when the gate is clean", () => {
|
|
74
|
+
expect(v.installable).toBe(true);
|
|
75
|
+
expect(agentsSummary(v)).toContain("✓ installable");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a dangling operationRef surfaces in the view AND blocks installability", () => {
|
|
79
|
+
const bad = structuredClone(doc);
|
|
80
|
+
bad["x-suluk-agents"]!.conin.routes!.generate_deliverable.operationRef = "#/paths/nope/requests/x";
|
|
81
|
+
const bv = agentsView(bad);
|
|
82
|
+
expect(bv.installable).toBe(false);
|
|
83
|
+
expect(bv.agents.find((a) => a.name === "conin")!.routes[0].resolves).toBe(false);
|
|
84
|
+
expect(bv.findings.some((f) => f.code === "dangling-operation-ref")).toBe(true);
|
|
85
|
+
expect(agentsSummary(bv)).toContain("blocking");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("context-budget load is attached, and unflatten surfaces when an agent is over budget (the add-a-layer check)", () => {
|
|
89
|
+
expect(v.agents.find((a) => a.name === "conin")!.context.totalTokens).toBeGreaterThan(0);
|
|
90
|
+
const tiny = structuredClone(doc);
|
|
91
|
+
tiny["x-suluk-agents"]!.conin.contextBudget = { tokens: 50, basis: "estimate" };
|
|
92
|
+
const v2 = agentsView(tiny);
|
|
93
|
+
expect(v2.contextFindings.some((f) => f.code === "context-over-budget")).toBe(true);
|
|
94
|
+
expect(v2.unflatten.some((u) => u.agent === "conin")).toBe(true);
|
|
95
|
+
expect(agentsSummary(v2)).toContain("unflatten");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("model selection surfaces 'why this model' when a catalog is supplied (C027 × @suluk/models)", () => {
|
|
99
|
+
const view = agentsView(doc, { catalog: SEED_CATALOG });
|
|
100
|
+
const conin = view.agents.find((a) => a.name === "conin")!;
|
|
101
|
+
expect(conin.modelSelection).toBeDefined();
|
|
102
|
+
const operate = conin.modelSelection!.find((m) => m.skill === "operate")!;
|
|
103
|
+
expect(operate.from).toBe("declared"); // operate has an explicit model[] → opt-out
|
|
104
|
+
expect(operate.ids).toEqual(["anthropic/claude-opus-4"]);
|
|
105
|
+
expect(agentsView(doc).agents[0].modelSelection).toBeUndefined(); // no catalog ⇒ no surface (back-compat)
|
|
106
|
+
|
|
107
|
+
const d = structuredClone(doc);
|
|
108
|
+
d["x-suluk-agents"]!.conin.skills!.operate = { modelProfile: "cheap-fast" };
|
|
109
|
+
const sel = agentsView(d, { catalog: SEED_CATALOG }).agents.find((a) => a.name === "conin")!.modelSelection!.find((m) => m.skill === "operate")!;
|
|
110
|
+
expect(sel.from).toBe("selected");
|
|
111
|
+
expect(sel.ids!.length).toBeGreaterThan(0);
|
|
112
|
+
expect(sel.resolve).toBe("pinned"); // C030 default, ungoverned
|
|
113
|
+
expect(sel.pickPinned).toBe(true);
|
|
114
|
+
expect(sel.decidingPreference).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("absent agent layer is handled", () => {
|
|
118
|
+
const empty = agentsView({ openapi: "4.0.0-candidate", info: { title: "x", version: "0" }, paths: {} });
|
|
119
|
+
expect(empty.present).toBe(false);
|
|
120
|
+
expect(agentsSummary(empty)).toContain("no agents");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("operator policy (C028): the OBSERVE diff shows declared vs effective + the cost three-number", () => {
|
|
124
|
+
const g = structuredClone(doc);
|
|
125
|
+
g["x-suluk-agents"]!.conin["x-suluk-cost"] = { estimateMicroUsd: 8000 };
|
|
126
|
+
g["x-suluk-policy"] = {
|
|
127
|
+
"acme-fleet": {
|
|
128
|
+
appliesTo: ["#/x-suluk-agents/conin"],
|
|
129
|
+
tools: { deny: ["generate_deliverable"] },
|
|
130
|
+
forbidNesting: true,
|
|
131
|
+
costCeiling: { amount: 5000, amountUnit: "micro-usd", basis: "per-request", enforcedBy: "adapter" },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const conin = agentsView(g).agents.find((a) => a.name === "conin")!;
|
|
135
|
+
expect(conin.governed).toBeDefined();
|
|
136
|
+
expect(conin.governed!.deniedTools).toEqual(["generate_deliverable"]);
|
|
137
|
+
expect(conin.governed!.nestingForbidden).toBe(true);
|
|
138
|
+
expect(conin.governed!.cost.cap).toContain("5000 micro-usd");
|
|
139
|
+
expect(conin.governed!.cost.cap).toContain("enforcedBy adapter"); // never reads as schema-enforced
|
|
140
|
+
expect(conin.governed!.cost.estimate).toBe("8000 µ$");
|
|
141
|
+
expect(conin.governed!.cost.actual).toContain("runtime");
|
|
142
|
+
expect(conin.governed!.narrowings.some((n) => n.axis === "tools")).toBe(true);
|
|
143
|
+
// an ungoverned agent has no diff
|
|
144
|
+
expect(agentsView(doc).agents.find((a) => a.name === "conin")!.governed).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|