@kindgi/capabilities 0.0.0-bootstrap.0 → 0.1.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.
@@ -0,0 +1,155 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ModelProvider, ProviderMetadata } from './types.js';
5
+
6
+ /**
7
+ * In-process factory-function registry — the code-side counterpart of a
8
+ * persistent adapter registry (`AdapterRegistryBinding` in
9
+ * `@kindgi/api`). A store can hold adapter records but not JavaScript
10
+ * functions, so the runtime keeps a Map keyed by the same adapter id
11
+ * the stored record uses.
12
+ *
13
+ * Populated at boot by deployment code, in lockstep with the adapters
14
+ * it registers in storage. A storage-backed provider registry reads a
15
+ * tenant's stored providers, looks up each one's adapter id here, calls
16
+ * the factory to instantiate a `ModelProvider`, and caches the result.
17
+ *
18
+ * Failure surfaces:
19
+ * - A stored provider references an adapter id with no in-process
20
+ * factory (deployment forgot to register it, or a boot ordering
21
+ * bug) → `get(adapterId)` returns `undefined`, and the provider
22
+ * registry fails the invocation with a clear error.
23
+ * - Factory throws at construction time → propagates through the
24
+ * provider registry to the caller.
25
+ */
26
+ export interface AdapterFactoryEntry {
27
+ /** Adapter identifier — MUST equal the id of the stored adapter record. */
28
+ readonly adapterId: string;
29
+ /** Capability kind this adapter fulfils (`'llm-inference'`, `'embedding'`, ...). */
30
+ readonly capabilityKind: string;
31
+ readonly factory: AdapterFactory;
32
+ /**
33
+ * Optional download / warmup step. Adapters that need
34
+ * expensive one-time setup (in-process model downloads, ONNX
35
+ * runtime warmup, cache preheating) implement this; adapters that
36
+ * only need config-time construction leave it undefined.
37
+ *
38
+ * Streams `PrepareEvent`s so the caller (HTTP SSE endpoint,
39
+ * console progress bar) can render progress. Framework doesn't
40
+ * cache — implementations decide whether re-invoking `prepare` on
41
+ * an already-prepared adapter is a no-op or a re-check.
42
+ *
43
+ * `params` shape is adapter-specific — the framework passes it
44
+ * through unchanged (e.g. `{model: 'smollm2-360m'}` for
45
+ * in-process).
46
+ */
47
+ readonly prepare?: (params?: Readonly<Record<string, unknown>>) => AsyncIterable<PrepareEvent>;
48
+ }
49
+
50
+ /**
51
+ * Event stream emitted by `AdapterFactoryEntry.prepare()`. Shape kept
52
+ * deliberately open (`message` + optional numeric progress fields)
53
+ * so adapters with different progress models (byte-count download,
54
+ * step-count warmup, cache hit ratio) can populate what they know.
55
+ * Consumers render whichever fields are present.
56
+ */
57
+ export type PrepareEvent =
58
+ | {
59
+ readonly kind: 'progress';
60
+ readonly message?: string;
61
+ /** Ratio in [0, 1] when known; absent for indeterminate progress. */
62
+ readonly ratio?: number;
63
+ readonly loadedBytes?: number;
64
+ readonly totalBytes?: number;
65
+ readonly file?: string;
66
+ }
67
+ | { readonly kind: 'ready'; readonly message?: string }
68
+ | { readonly kind: 'error'; readonly message: string };
69
+
70
+ /**
71
+ * Input passed to an adapter factory at instantiation time. The
72
+ * provider registry composes the arguments from persisted state (the
73
+ * stored provider `metadata`) plus a pre-bound `resolveApiKey` closure
74
+ * that reads the tenant-scoped secret store.
75
+ *
76
+ * Adapters that don't need credentials (dev-echo, in-process
77
+ * models) receive no `resolveApiKey` — they ignore the field
78
+ * entirely.
79
+ */
80
+ export interface AdapterFactoryInput {
81
+ readonly metadata: ProviderMetadata;
82
+ /**
83
+ * How the adapter connects — the provider's adapter-specific,
84
+ * non-secret settings (a cloud project, a base URL), as registered
85
+ * with the provider. Credentials never go here; they come from
86
+ * `resolveApiKey`. Each adapter documents and validates its own keys.
87
+ */
88
+ readonly config?: AdapterConfig;
89
+ /**
90
+ * Lazy API-key resolver. Adapters that need external
91
+ * credentials pass this into their SDK client construction (or
92
+ * refresh on every invoke, as
93
+ * `@kindgi/adapter-model-anthropic` does).
94
+ */
95
+ readonly resolveApiKey?: () => Promise<string>;
96
+ /**
97
+ * The fetch for a provider endpoint the registration chose (a base URL).
98
+ * The runtime passes one that refuses the hosts its deployment forbids
99
+ * (`KINDGI_TENANT_HOST_ACCESS`: the cloud metadata endpoints and the
100
+ * server's own host). Absent: the global `fetch`.
101
+ */
102
+ readonly fetch?: typeof fetch;
103
+ }
104
+
105
+ /** An adapter's connection settings: flat, non-secret values. */
106
+ export type AdapterConfig = Readonly<Record<string, string | number | boolean>>;
107
+
108
+ export type AdapterFactory = (input: AdapterFactoryInput) => ModelProvider;
109
+
110
+ export interface AdapterFactoryRegistry {
111
+ register(entry: AdapterFactoryEntry): void;
112
+ get(adapterId: string): AdapterFactoryEntry | undefined;
113
+ has(adapterId: string): boolean;
114
+ list(): readonly AdapterFactoryEntry[];
115
+ }
116
+
117
+ /**
118
+ * Create an in-memory `AdapterFactoryRegistry`. Global
119
+ * (deployment-scoped) — every tenant reads from the same map because
120
+ * adapter code is code, not user data.
121
+ *
122
+ * Duplicate registrations under the same `adapterId` throw at
123
+ * `register` time; accidental clobber of a running factory would be
124
+ * catastrophic. Deployments call `register` once per shipped adapter
125
+ * at boot.
126
+ */
127
+ export function createAdapterFactoryRegistry(
128
+ seed: readonly AdapterFactoryEntry[] = [],
129
+ ): AdapterFactoryRegistry {
130
+ const entries = new Map<string, AdapterFactoryEntry>();
131
+
132
+ function register(entry: AdapterFactoryEntry): void {
133
+ if (entries.has(entry.adapterId)) {
134
+ throw new Error(
135
+ `AdapterFactoryRegistry: "${entry.adapterId}" is already registered. Duplicate adapter registration at boot indicates a wiring bug.`,
136
+ );
137
+ }
138
+ entries.set(entry.adapterId, entry);
139
+ }
140
+
141
+ for (const entry of seed) register(entry);
142
+
143
+ return {
144
+ register,
145
+ get(adapterId): AdapterFactoryEntry | undefined {
146
+ return entries.get(adapterId);
147
+ },
148
+ has(adapterId): boolean {
149
+ return entries.has(adapterId);
150
+ },
151
+ list(): readonly AdapterFactoryEntry[] {
152
+ return [...entries.values()];
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,250 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kindgi.com/schemas/v1/capability.schema.json",
4
+ "$comment": "schema-version: 1.1.0",
5
+ "title": "Capability declaration",
6
+ "description": "An agent's model requirements as declarative constraints. The model router matches capabilities against registered providers, respecting per-tenant policy. `defineCapability` validates a declaration against this schema; a declaration that no registered provider satisfies fails at routing time with `capability-unsatisfiable`.",
7
+ "type": "object",
8
+ "required": ["needs"],
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "kind": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "default": "llm-inference",
15
+ "examples": ["llm-inference", "embedding", "gpu-compute", "sandbox-exec", "browser-session"],
16
+ "description": "Resource kind this capability requests. Open string: `examples` lists the well-known kinds, and adapters can declare their own. Absent means `llm-inference`. The router only considers providers whose `capabilityKind` equals it (a provider that declares none counts as `llm-inference`). Added in schema-version 1.1.0."
17
+ },
18
+ "needs": {
19
+ "type": "array",
20
+ "minItems": 1,
21
+ "items": {
22
+ "$ref": "#/$defs/Requirement"
23
+ }
24
+ },
25
+ "prefer": {
26
+ "type": "array",
27
+ "items": {
28
+ "$ref": "#/$defs/Preference"
29
+ },
30
+ "description": "Soft preferences: the router picks the highest-scoring model that also satisfies all 'needs'."
31
+ },
32
+ "budget": {
33
+ "$ref": "#/$defs/Budget",
34
+ "description": "Per-invocation budget. Declarative: carried with the declaration but not enforced by the runtime (per-turn spend is capped by the agent's `budget.maxCostUsd`)."
35
+ }
36
+ },
37
+ "$defs": {
38
+ "Requirement": {
39
+ "oneOf": [
40
+ {
41
+ "$ref": "#/$defs/FeatureRequirement"
42
+ },
43
+ {
44
+ "$ref": "#/$defs/ContextRequirement"
45
+ },
46
+ {
47
+ "$ref": "#/$defs/CostRequirement"
48
+ },
49
+ {
50
+ "$ref": "#/$defs/RegionRequirement"
51
+ },
52
+ {
53
+ "$ref": "#/$defs/LatencyRequirement"
54
+ },
55
+ {
56
+ "$ref": "#/$defs/ProviderRequirement"
57
+ },
58
+ {
59
+ "$ref": "#/$defs/ModelRequirement"
60
+ }
61
+ ]
62
+ },
63
+ "FeatureRequirement": {
64
+ "type": "object",
65
+ "required": ["feature"],
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "feature": {
69
+ "type": "string",
70
+ "enum": [
71
+ "structured-output",
72
+ "vision",
73
+ "audio-input",
74
+ "audio-output",
75
+ "tool-use",
76
+ "parallel-tool-use",
77
+ "thinking",
78
+ "long-context",
79
+ "code-execution",
80
+ "web-search",
81
+ "file-search",
82
+ "streaming",
83
+ "batch"
84
+ ]
85
+ }
86
+ }
87
+ },
88
+ "ContextRequirement": {
89
+ "type": "object",
90
+ "required": ["contextWindow"],
91
+ "additionalProperties": false,
92
+ "properties": {
93
+ "contextWindow": {
94
+ "type": "object",
95
+ "required": ["op", "value"],
96
+ "additionalProperties": false,
97
+ "properties": {
98
+ "op": {
99
+ "type": "string",
100
+ "enum": [">=", ">", "=", "<=", "<"]
101
+ },
102
+ "value": {
103
+ "type": "integer",
104
+ "minimum": 1
105
+ }
106
+ }
107
+ }
108
+ }
109
+ },
110
+ "CostRequirement": {
111
+ "type": "object",
112
+ "required": ["costPerCall"],
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "costPerCall": {
116
+ "type": "object",
117
+ "required": ["op", "usd"],
118
+ "additionalProperties": false,
119
+ "properties": {
120
+ "op": {
121
+ "type": "string",
122
+ "enum": [">=", ">", "=", "<=", "<"]
123
+ },
124
+ "usd": {
125
+ "type": "number",
126
+ "minimum": 0
127
+ }
128
+ }
129
+ }
130
+ }
131
+ },
132
+ "RegionRequirement": {
133
+ "type": "object",
134
+ "required": ["region"],
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "region": {
138
+ "type": "string",
139
+ "description": "Geographic region constraint (e.g. 'eu-only', 'us-only', 'us-gov'). Enforced by the router against each provider's declared region; a tenant policy's `regionAllow` can constrain it further."
140
+ }
141
+ }
142
+ },
143
+ "LatencyRequirement": {
144
+ "type": "object",
145
+ "required": ["p95LatencyMs"],
146
+ "additionalProperties": false,
147
+ "properties": {
148
+ "p95LatencyMs": {
149
+ "type": "object",
150
+ "required": ["op", "value"],
151
+ "additionalProperties": false,
152
+ "properties": {
153
+ "op": {
154
+ "type": "string",
155
+ "enum": ["<=", "<"]
156
+ },
157
+ "value": {
158
+ "type": "integer",
159
+ "minimum": 0
160
+ }
161
+ }
162
+ }
163
+ }
164
+ },
165
+ "ProviderRequirement": {
166
+ "type": "object",
167
+ "required": ["providers"],
168
+ "additionalProperties": false,
169
+ "description": "Hard constraint on allowed providers (allowlist) or disallowed providers (denylist). Tenant policy can narrow the set further.",
170
+ "properties": {
171
+ "providers": {
172
+ "type": "object",
173
+ "additionalProperties": false,
174
+ "properties": {
175
+ "allow": {
176
+ "type": "array",
177
+ "items": {
178
+ "type": "string"
179
+ }
180
+ },
181
+ "deny": {
182
+ "type": "array",
183
+ "items": {
184
+ "type": "string"
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ },
191
+ "ModelRequirement": {
192
+ "type": "object",
193
+ "required": ["models"],
194
+ "additionalProperties": false,
195
+ "description": "Hard constraint on allowed models (allowlist) or disallowed models (denylist), matched against each provider's model names (`ModelInfo.name`). Can be combined with a providers requirement. Tenant policy can narrow the set further. Added in schema-version 1.1.0.",
196
+ "properties": {
197
+ "models": {
198
+ "type": "object",
199
+ "additionalProperties": false,
200
+ "properties": {
201
+ "allow": {
202
+ "type": "array",
203
+ "items": {
204
+ "type": "string"
205
+ }
206
+ },
207
+ "deny": {
208
+ "type": "array",
209
+ "items": {
210
+ "type": "string"
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ },
217
+ "Preference": {
218
+ "type": "object",
219
+ "required": ["feature", "weight"],
220
+ "additionalProperties": false,
221
+ "properties": {
222
+ "feature": {
223
+ "type": "string",
224
+ "description": "A capability feature or a soft attribute (e.g. 'lower-cost', 'lower-latency', 'higher-accuracy')."
225
+ },
226
+ "weight": {
227
+ "type": "number"
228
+ }
229
+ }
230
+ },
231
+ "Budget": {
232
+ "type": "object",
233
+ "additionalProperties": false,
234
+ "properties": {
235
+ "maxCostUsd": {
236
+ "type": "number",
237
+ "minimum": 0
238
+ },
239
+ "maxTokens": {
240
+ "type": "integer",
241
+ "minimum": 0
242
+ },
243
+ "maxDurationMs": {
244
+ "type": "integer",
245
+ "minimum": 0
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
package/src/define.ts ADDED
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { createSpecRegistry } from '@kindgi/schema';
5
+ import type { SpecRegistry } from '@kindgi/schema';
6
+ import type { Result } from '@kindgi/types';
7
+
8
+ import capabilitySchema from './capability.schema.json' with { type: 'json' };
9
+ import type { InvalidCapabilityError } from './errors.js';
10
+ import type { Capability } from './types.js';
11
+
12
+ const CAPABILITY_SCHEMA_ID = 'https://kindgi.com/schemas/v1/capability.schema.json';
13
+
14
+ let cachedRegistry: SpecRegistry | undefined;
15
+ function registry(): SpecRegistry {
16
+ if (cachedRegistry !== undefined) return cachedRegistry;
17
+ const built = createSpecRegistry([capabilitySchema]);
18
+ if (built.kind === 'err') {
19
+ throw new Error(
20
+ `@kindgi/capabilities: bundled schema failed to compile: ${built.error.message}`,
21
+ );
22
+ }
23
+ cachedRegistry = built.value;
24
+ return cachedRegistry;
25
+ }
26
+
27
+ /**
28
+ * Validate a capability declaration against `@kindgi/specs/capability.schema.json`
29
+ * and return a typed `Capability` on success. Called by pack authors at
30
+ * agent construction — a rejection here surfaces at pack init, well
31
+ * before any user request touches the flow.
32
+ */
33
+ export function defineCapability(spec: Capability): Result<Capability, InvalidCapabilityError> {
34
+ const r = registry().validate<Capability>(CAPABILITY_SCHEMA_ID, spec);
35
+ if (r.kind === 'ok') return r;
36
+ const err = r.error;
37
+ if (err.code !== 'validation-error') {
38
+ throw new Error(`@kindgi/capabilities: unexpected schema error ${err.code}: ${err.message}`);
39
+ }
40
+ const issues = err.errors.map((e) => {
41
+ const ajv = e as { instancePath?: unknown; message?: unknown };
42
+ return {
43
+ path: typeof ajv.instancePath === 'string' ? ajv.instancePath : '',
44
+ message: typeof ajv.message === 'string' ? ajv.message : 'validation failed',
45
+ };
46
+ });
47
+ return {
48
+ kind: 'err',
49
+ error: { code: 'invalid-capability', message: err.message, issues },
50
+ };
51
+ }
52
+
53
+ /** The `$id` of the JSON Schema this loader validates against. */
54
+ export const CAPABILITY_SCHEMA_URI = CAPABILITY_SCHEMA_ID;
package/src/errors.ts ADDED
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { TenantId } from '@kindgi/types';
5
+
6
+ /**
7
+ * Errors emitted by @kindgi/capabilities. Every variant carries a `code`
8
+ * for pattern matching; messages are human-readable, not API contract.
9
+ */
10
+ export type CapabilityError =
11
+ | InvalidCapabilityError
12
+ | InvalidProviderError
13
+ | DuplicateProviderError
14
+ | CapabilityUnsatisfiableError
15
+ | BudgetExceededError;
16
+
17
+ /** `defineCapability` rejected the declaration shape (schema mismatch). */
18
+ export interface InvalidCapabilityError {
19
+ readonly code: 'invalid-capability';
20
+ readonly message: string;
21
+ readonly issues: readonly { readonly path: string; readonly message: string }[];
22
+ }
23
+
24
+ /** Provider registration (`createProviderRegistry(...).register`) rejected the metadata shape. */
25
+ export interface InvalidProviderError {
26
+ readonly code: 'invalid-provider';
27
+ readonly message: string;
28
+ readonly reason: string;
29
+ }
30
+
31
+ /** Provider registration was called with an id already registered for the tenant. */
32
+ export interface DuplicateProviderError {
33
+ readonly code: 'duplicate-provider';
34
+ readonly message: string;
35
+ readonly providerId: string;
36
+ }
37
+
38
+ /**
39
+ * The router couldn't find any provider satisfying the capability
40
+ * declaration + tenant policy. `reasons` explains which requirement
41
+ * rejected which `(provider, model)` candidate.
42
+ */
43
+ export interface CapabilityUnsatisfiableError {
44
+ readonly code: 'capability-unsatisfiable';
45
+ readonly message: string;
46
+ /**
47
+ * One entry per requirement, indicating which providers matched
48
+ * and which didn't. Each rejection carries a structured
49
+ * `RejectionReason` — callers can `switch (r.reason.code)` to route
50
+ * on specific failure modes (e.g. surface an "add region" tip when
51
+ * every failure is `region-mismatch`).
52
+ */
53
+ readonly reasons: readonly {
54
+ readonly requirement: string;
55
+ readonly satisfyingProviders: readonly string[];
56
+ readonly rejectingProviders: readonly {
57
+ readonly id: string;
58
+ readonly reason: import('./types.js').RejectionReason;
59
+ }[];
60
+ }[];
61
+ readonly tenantId?: TenantId;
62
+ }
63
+
64
+ /**
65
+ * A model invocation exceeded the caller's declared `budget`. Cost meter
66
+ * emits this either pre-flight (if the estimated cost is already over
67
+ * budget) or post-flight (if actual cost overran).
68
+ */
69
+ export interface BudgetExceededError {
70
+ readonly code: 'budget-exceeded';
71
+ readonly message: string;
72
+ readonly limit: { readonly kind: 'cost' | 'tokens' | 'duration'; readonly value: number };
73
+ readonly actual: number;
74
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ export { createAdapterFactoryRegistry } from './adapter-factory.js';
5
+ export type {
6
+ AdapterConfig,
7
+ AdapterFactory,
8
+ AdapterFactoryEntry,
9
+ AdapterFactoryInput,
10
+ AdapterFactoryRegistry,
11
+ PrepareEvent,
12
+ } from './adapter-factory.js';
13
+ export { CAPABILITY_SCHEMA_URI, defineCapability } from './define.js';
14
+ export { createProviderRegistry } from './registry.js';
15
+ export { matchTuples, route } from './router.js';
16
+ export type { RouteInput } from './router.js';
17
+ export { BUILT_IN_CAPABILITY_KINDS, DEFAULT_CAPABILITY_KIND, FEATURES } from './types.js';
18
+ export type {
19
+ Budget,
20
+ BuiltInCapabilityKind,
21
+ Capability,
22
+ CapabilityKind,
23
+ ComparisonOp,
24
+ Feature,
25
+ ModelCallInput,
26
+ ModelCallResult,
27
+ ModelMessage,
28
+ ModelInfo,
29
+ ModelProvider,
30
+ ModelToolCall,
31
+ ModelToolDefinition,
32
+ Preference,
33
+ ProviderMetadata,
34
+ ProviderModelPick,
35
+ ProviderRegistry,
36
+ RejectionReason,
37
+ Requirement,
38
+ RoutingDecision,
39
+ StructuredOutputRequest,
40
+ TenantPolicy,
41
+ UpperBoundOp,
42
+ UsageCounters,
43
+ } from './types.js';
44
+ export type {
45
+ BudgetExceededError,
46
+ CapabilityError,
47
+ CapabilityUnsatisfiableError,
48
+ DuplicateProviderError,
49
+ InvalidCapabilityError,
50
+ InvalidProviderError,
51
+ } from './errors.js';