@kindgi/capabilities 0.0.0-bootstrap.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/LICENSE +201 -0
- package/README.md +112 -2
- package/dist/adapter-factory.d.ts +121 -0
- package/dist/adapter-factory.d.ts.map +1 -0
- package/dist/adapter-factory.js +36 -0
- package/dist/adapter-factory.js.map +1 -0
- package/dist/capability.schema.json +250 -0
- package/dist/define.d.ts +13 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +45 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +156 -0
- package/dist/registry.js.map +1 -0
- package/dist/router.d.ts +73 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +484 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +498 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -4
- package/src/adapter-factory.ts +155 -0
- package/src/capability.schema.json +250 -0
- package/src/define.ts +54 -0
- package/src/errors.ts +74 -0
- package/src/index.ts +51 -0
- package/src/registry.ts +179 -0
- package/src/router.ts +560 -0
- package/src/types.ts +533 -0
|
@@ -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';
|