@kindgi/api 0.1.3 → 0.1.4-rc.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.
- package/dist/agent-binding.d.ts +18 -4
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts +48 -0
- package/dist/agent-pins.d.ts.map +1 -0
- package/dist/agent-pins.js +102 -0
- package/dist/agent-pins.js.map +1 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +20 -2
- package/dist/app.js.map +1 -1
- package/dist/block-binding.d.ts +132 -0
- package/dist/block-binding.d.ts.map +1 -0
- package/dist/block-binding.js +4 -0
- package/dist/block-binding.js.map +1 -0
- package/dist/block-pins.d.ts +22 -0
- package/dist/block-pins.d.ts.map +1 -0
- package/dist/block-pins.js +112 -0
- package/dist/block-pins.js.map +1 -0
- package/dist/cost-binding.d.ts +20 -1
- package/dist/cost-binding.d.ts.map +1 -1
- package/dist/cost-binding.js +4 -0
- package/dist/cost-binding.js.map +1 -1
- package/dist/deploy-versions.d.ts +58 -0
- package/dist/deploy-versions.d.ts.map +1 -0
- package/dist/deploy-versions.js +91 -0
- package/dist/deploy-versions.js.map +1 -0
- package/dist/deployment-binding.d.ts +24 -3
- package/dist/deployment-binding.d.ts.map +1 -1
- package/dist/derive-agent-version.d.ts +69 -0
- package/dist/derive-agent-version.d.ts.map +1 -0
- package/dist/derive-agent-version.js +139 -0
- package/dist/derive-agent-version.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +65 -0
- package/dist/eval-case-binding.d.ts.map +1 -0
- package/dist/eval-case-binding.js +4 -0
- package/dist/eval-case-binding.js.map +1 -0
- package/dist/eval-run-binding.d.ts +30 -0
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +38 -3
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js +21 -15
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/eval-suite-binding.d.ts +1 -1
- package/dist/eval-suite-binding.d.ts.map +1 -1
- package/dist/eval-suite-binding.js +2 -0
- package/dist/eval-suite-binding.js.map +1 -1
- package/dist/flow-binding.d.ts +10 -4
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts +36 -0
- package/dist/flow-pins.d.ts.map +1 -0
- package/dist/flow-pins.js +81 -0
- package/dist/flow-pins.js.map +1 -0
- package/dist/index.d.ts +17 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +134 -0
- package/dist/judged-dispatcher.d.ts.map +1 -0
- package/dist/judged-dispatcher.js +297 -0
- package/dist/judged-dispatcher.js.map +1 -0
- package/dist/judged-items.d.ts +86 -0
- package/dist/judged-items.d.ts.map +1 -0
- package/dist/judged-items.js +184 -0
- package/dist/judged-items.js.map +1 -0
- package/dist/judgment-binding.d.ts +316 -0
- package/dist/judgment-binding.d.ts.map +1 -0
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -1
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +461 -7
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +40 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +843 -7
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/provider-binding.d.ts +12 -7
- package/dist/provider-binding.d.ts.map +1 -1
- package/dist/routes/agents.d.ts +9 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +175 -11
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/blocks.d.ts +19 -0
- package/dist/routes/blocks.d.ts.map +1 -0
- package/dist/routes/blocks.js +281 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +47 -2
- package/dist/routes/cost.js.map +1 -1
- package/dist/routes/deployments.d.ts +3 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +208 -55
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +14 -0
- package/dist/routes/eval-comparison.d.ts.map +1 -0
- package/dist/routes/eval-comparison.js +87 -0
- package/dist/routes/eval-comparison.js.map +1 -0
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +8 -0
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/flows.d.ts +13 -1
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +44 -3
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/hierarchy-errors.d.ts +35 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -0
- package/dist/routes/hierarchy-errors.js +39 -0
- package/dist/routes/hierarchy-errors.js.map +1 -0
- package/dist/routes/judged-suites.d.ts +20 -0
- package/dist/routes/judged-suites.d.ts.map +1 -0
- package/dist/routes/judged-suites.js +272 -0
- package/dist/routes/judged-suites.js.map +1 -0
- package/dist/routes/judgment-context.d.ts +22 -0
- package/dist/routes/judgment-context.d.ts.map +1 -0
- package/dist/routes/judgment-context.js +88 -0
- package/dist/routes/judgment-context.js.map +1 -0
- package/dist/routes/judgment-flow-context.d.ts +32 -0
- package/dist/routes/judgment-flow-context.d.ts.map +1 -0
- package/dist/routes/judgment-flow-context.js +195 -0
- package/dist/routes/judgment-flow-context.js.map +1 -0
- package/dist/routes/judgments.d.ts +41 -0
- package/dist/routes/judgments.d.ts.map +1 -0
- package/dist/routes/judgments.js +566 -0
- package/dist/routes/judgments.js.map +1 -0
- package/dist/routes/orgs.d.ts +5 -2
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +38 -22
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +12 -1
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts +10 -2
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +87 -79
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +6 -1
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/runs.js +24 -3
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/teams.d.ts +6 -2
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +77 -73
- package/dist/routes/teams.js.map +1 -1
- package/openapi.json +13545 -10103
- package/package.json +21 -21
- package/src/agent-binding.ts +19 -4
- package/src/agent-pins.ts +147 -0
- package/src/app.ts +71 -2
- package/src/block-binding.ts +137 -0
- package/src/block-pins.ts +148 -0
- package/src/cost-binding.ts +21 -1
- package/src/deploy-versions.ts +157 -0
- package/src/deployment-binding.ts +27 -3
- package/src/derive-agent-version.ts +206 -0
- package/src/errors.ts +17 -0
- package/src/eval-case-binding.ts +71 -0
- package/src/eval-run-binding.ts +33 -0
- package/src/eval-run-dispatcher.ts +57 -16
- package/src/eval-suite-binding.ts +2 -0
- package/src/flow-binding.ts +11 -4
- package/src/flow-pins.ts +113 -0
- package/src/index.ts +85 -2
- package/src/judged-dispatcher.ts +507 -0
- package/src/judged-items.ts +263 -0
- package/src/judgment-binding.ts +349 -0
- package/src/openapi/generate.ts +7 -1
- package/src/openapi/operations.ts +523 -7
- package/src/openapi/schemas.ts +1009 -88
- package/src/provider-binding.ts +12 -7
- package/src/routes/agents.ts +243 -19
- package/src/routes/blocks.ts +362 -0
- package/src/routes/cost.ts +55 -1
- package/src/routes/deployments.ts +266 -56
- package/src/routes/eval-comparison.ts +101 -0
- package/src/routes/eval-runs.ts +11 -0
- package/src/routes/flows.ts +63 -5
- package/src/routes/hierarchy-errors.ts +51 -0
- package/src/routes/judged-suites.ts +363 -0
- package/src/routes/judgment-context.ts +128 -0
- package/src/routes/judgment-flow-context.ts +245 -0
- package/src/routes/judgments.ts +743 -0
- package/src/routes/orgs.ts +44 -27
- package/src/routes/policies.ts +19 -0
- package/src/routes/projects.ts +106 -95
- package/src/routes/providers.ts +5 -0
- package/src/routes/runs.ts +28 -3
- package/src/routes/teams.ts +96 -90
package/src/cost-binding.ts
CHANGED
|
@@ -44,7 +44,10 @@ export interface CostBinding {
|
|
|
44
44
|
* Multi-dimensional aggregate rollup. `groupBy` may combine any
|
|
45
45
|
* subset of `COST_GROUP_DIMENSIONS`; the binding returns one group per
|
|
46
46
|
* distinct key tuple within the required `from`..`to` window, with its
|
|
47
|
-
* cost and token sums.
|
|
47
|
+
* cost and token sums. With `limit`, a binding may return only the
|
|
48
|
+
* `limit` most expensive groups (`totalUsd` descending, ties by key)
|
|
49
|
+
* and the count before the cap in `totalGroups`; one that returns
|
|
50
|
+
* every group is capped by the route.
|
|
48
51
|
*/
|
|
49
52
|
aggregate(input: CostAggregateInput): Promise<CostAggregateResult>;
|
|
50
53
|
}
|
|
@@ -195,8 +198,20 @@ export interface CostAggregateInput {
|
|
|
195
198
|
* shape uniformity.
|
|
196
199
|
*/
|
|
197
200
|
readonly inherit?: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* The most groups the caller wants (`?limit=`, 1..`COST_AGGREGATE_MAX_LIMIT`,
|
|
203
|
+
* default `COST_AGGREGATE_DEFAULT_LIMIT`): the most expensive ones. A
|
|
204
|
+
* binding may cap in its query; the window's totals stay over every
|
|
205
|
+
* record either way.
|
|
206
|
+
*/
|
|
207
|
+
readonly limit?: number;
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
/** `GET /v1/cost/aggregate`'s `?limit=` when absent. */
|
|
211
|
+
export const COST_AGGREGATE_DEFAULT_LIMIT = 1000;
|
|
212
|
+
/** The largest `?limit=` the aggregate takes. */
|
|
213
|
+
export const COST_AGGREGATE_MAX_LIMIT = 10_000;
|
|
214
|
+
|
|
200
215
|
/**
|
|
201
216
|
* One row of the aggregate result. `key` maps every requested `groupBy`
|
|
202
217
|
* dimension to its value for this group — `null` when the underlying
|
|
@@ -226,6 +241,11 @@ export interface CostTokenTotals {
|
|
|
226
241
|
|
|
227
242
|
export interface CostAggregateResult {
|
|
228
243
|
readonly groups: readonly CostAggregateGroup[];
|
|
244
|
+
/**
|
|
245
|
+
* How many groups there were before a cap: set by a binding that caps
|
|
246
|
+
* (`CostAggregateInput.limit`). Absent: `groups` is every group.
|
|
247
|
+
*/
|
|
248
|
+
readonly totalGroups?: number;
|
|
229
249
|
readonly totalUsd: number;
|
|
230
250
|
readonly totalRecords: number;
|
|
231
251
|
readonly tokens: CostTokenTotals;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { type PinChange, type PinSet, pinChanges } from '@kindgi/agents';
|
|
5
|
+
import { canonicalize } from '@kindgi/schema';
|
|
6
|
+
import { nextVersion } from '@kindgi/tools';
|
|
7
|
+
import type { VersionDerivation, VersionDerivationReason } from '@kindgi/types';
|
|
8
|
+
|
|
9
|
+
/** A version a deploy registers with pins: an agent's or a flow's. */
|
|
10
|
+
export interface PinnedDefinition {
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly pins?: PinSet;
|
|
13
|
+
readonly pinsDigest?: string;
|
|
14
|
+
readonly derivedFrom?: VersionDerivation;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How a deploy registered one of its agents or flows, given its pins.
|
|
19
|
+
* A deploy never keeps a version's old pins and never refuses a
|
|
20
|
+
* routine deploy:
|
|
21
|
+
*
|
|
22
|
+
* - `registered`: the definition's version was free, and is now
|
|
23
|
+
* registered with these pins;
|
|
24
|
+
* - `unchanged`: it's registered with the same definition and pins (or
|
|
25
|
+
* its project is gone, which a deploy has always left as it is);
|
|
26
|
+
* - `reused`: an earlier deploy already registered this definition and
|
|
27
|
+
* these pins under another number (`version`), so a redeploy changes
|
|
28
|
+
* nothing;
|
|
29
|
+
* - `renumbered`: the definition's version is registered with other
|
|
30
|
+
* pins or other content, and versions never change, so this deploy
|
|
31
|
+
* registered the next free version in its line (`nextVersion`).
|
|
32
|
+
*/
|
|
33
|
+
export type DeployedVersionOutcome =
|
|
34
|
+
| { readonly kind: 'registered' | 'unchanged'; readonly version: string }
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: 'reused' | 'renumbered';
|
|
37
|
+
readonly version: string;
|
|
38
|
+
readonly reason: VersionDerivationReason;
|
|
39
|
+
readonly pinChanges?: readonly PinChange[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** What registering one version did: stored it, found the number taken, or left it (no project). */
|
|
43
|
+
export type PublishOutcome = 'ok' | 'taken' | 'skipped';
|
|
44
|
+
|
|
45
|
+
export interface DeployVersionInput<T extends PinnedDefinition> {
|
|
46
|
+
/** Names the definition in an error, e.g. `agent "acme.matcher"`. */
|
|
47
|
+
readonly label: string;
|
|
48
|
+
/** The definition as the pack has it, with no pins. */
|
|
49
|
+
readonly definition: T;
|
|
50
|
+
readonly pins: PinSet;
|
|
51
|
+
readonly pinsDigest: string;
|
|
52
|
+
/** Every active version registered under the definition's id. */
|
|
53
|
+
readonly existing: readonly T[];
|
|
54
|
+
/** Register one version. */
|
|
55
|
+
readonly publish: (version: T) => Promise<PublishOutcome>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** How many versions after the definition's a deploy tries before it gives up. */
|
|
59
|
+
const MAX_RENUMBER = 100;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register a deployed agent or flow with its pins (see
|
|
63
|
+
* `DeployedVersionOutcome`): one rule for both, so they can't drift
|
|
64
|
+
* apart. Each definition is registered at most once per deploy, and a
|
|
65
|
+
* new version derives from the definition's version only, never from a
|
|
66
|
+
* version the same deploy created, so a change cascading from a tool
|
|
67
|
+
* through an agent into a flow ends within the one deploy.
|
|
68
|
+
*/
|
|
69
|
+
export async function deployVersion<T extends PinnedDefinition>(
|
|
70
|
+
input: DeployVersionInput<T>,
|
|
71
|
+
): Promise<DeployedVersionOutcome> {
|
|
72
|
+
const { definition, pins, pinsDigest, existing } = input;
|
|
73
|
+
const authored = definition.version;
|
|
74
|
+
const key = definitionKey(definition);
|
|
75
|
+
const same = (v: T) => definitionKey(v) === key && v.pinsDigest === pinsDigest;
|
|
76
|
+
|
|
77
|
+
const atAuthored = existing.find((v) => v.version === authored);
|
|
78
|
+
if (atAuthored !== undefined && same(atAuthored)) return { kind: 'unchanged', version: authored };
|
|
79
|
+
if (atAuthored === undefined) {
|
|
80
|
+
const outcome = await input.publish({ ...definition, pins, pinsDigest });
|
|
81
|
+
if (outcome !== 'taken') {
|
|
82
|
+
return { kind: outcome === 'ok' ? 'registered' : 'unchanged', version: authored };
|
|
83
|
+
}
|
|
84
|
+
// Taken with no active version: an unregistered version holds the
|
|
85
|
+
// number. Register the next one.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const reason = derivationReason(atAuthored, key);
|
|
89
|
+
const changes =
|
|
90
|
+
reason === 'pins-changed' ? { pinChanges: pinChanges(atAuthored?.pins, pins) } : {};
|
|
91
|
+
const earlier = existing.find(same);
|
|
92
|
+
if (earlier !== undefined) {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'reused',
|
|
95
|
+
version: earlier.version,
|
|
96
|
+
// An expert's edit reused by a deploy reports the deploy's own reason.
|
|
97
|
+
reason:
|
|
98
|
+
earlier.derivedFrom !== undefined && earlier.derivedFrom.reason !== 'edited'
|
|
99
|
+
? earlier.derivedFrom.reason
|
|
100
|
+
: reason,
|
|
101
|
+
...changes,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const taken = new Set(existing.map((v) => v.version));
|
|
105
|
+
const version = await registerNextFree(input, reason, taken);
|
|
106
|
+
return version === undefined
|
|
107
|
+
? { kind: 'unchanged', version: authored }
|
|
108
|
+
: { kind: 'renumbered', version, reason, ...changes };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Why the definition's version can't be registered as it is. */
|
|
112
|
+
function derivationReason(
|
|
113
|
+
atAuthored: PinnedDefinition | undefined,
|
|
114
|
+
key: string,
|
|
115
|
+
): VersionDerivationReason {
|
|
116
|
+
if (atAuthored === undefined || definitionKey(atAuthored) !== key) return 'version-taken';
|
|
117
|
+
return atAuthored.pins === undefined ? 'unpinned' : 'pins-changed';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register the definition with its pins under the first free version
|
|
122
|
+
* after its own (`nextVersion`), recording where it came from.
|
|
123
|
+
* `undefined` when it was left as it is (its project is gone).
|
|
124
|
+
*/
|
|
125
|
+
async function registerNextFree<T extends PinnedDefinition>(
|
|
126
|
+
input: DeployVersionInput<T>,
|
|
127
|
+
reason: VersionDerivationReason,
|
|
128
|
+
taken: ReadonlySet<string>,
|
|
129
|
+
): Promise<string | undefined> {
|
|
130
|
+
const { definition, pins, pinsDigest } = input;
|
|
131
|
+
const authored = definition.version;
|
|
132
|
+
let candidate = nextVersion(authored);
|
|
133
|
+
for (let tries = 0; candidate !== undefined && tries < MAX_RENUMBER; tries++) {
|
|
134
|
+
if (!taken.has(candidate)) {
|
|
135
|
+
const outcome = await input.publish({
|
|
136
|
+
...definition,
|
|
137
|
+
version: candidate,
|
|
138
|
+
pins,
|
|
139
|
+
pinsDigest,
|
|
140
|
+
derivedFrom: { version: authored, reason },
|
|
141
|
+
});
|
|
142
|
+
if (outcome === 'ok') return candidate;
|
|
143
|
+
if (outcome === 'skipped') return undefined;
|
|
144
|
+
// Taken: an unregistered version holds this one too.
|
|
145
|
+
}
|
|
146
|
+
candidate = nextVersion(candidate);
|
|
147
|
+
}
|
|
148
|
+
throw new Error(
|
|
149
|
+
`${input.label}: no free version after ${authored} to register its new pins under`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A definition: everything but its version and what the runtime sets. */
|
|
154
|
+
function definitionKey(definition: PinnedDefinition): string {
|
|
155
|
+
const { version: _v, pins: _p, pinsDigest: _d, derivedFrom: _f, ...rest } = definition;
|
|
156
|
+
return canonicalize(rest);
|
|
157
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { PinChange } from '@kindgi/agents';
|
|
5
|
+
import type { Cursor, SigningKeyId, TenantId, VersionDerivationReason } from '@kindgi/types';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Caller-plugged surface for the deployment ledger — the audit anchor
|
|
@@ -127,11 +128,34 @@ export interface DeployedPrimitive {
|
|
|
127
128
|
readonly version?: string;
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/**
|
|
132
|
+
* An agent or flow a deployment shipped, under the version it's
|
|
133
|
+
* registered as. A deploy registers one under another version than its
|
|
134
|
+
* definition's when that version is registered already with other pins
|
|
135
|
+
* or content (versions never change); then `authoredVersion` is the
|
|
136
|
+
* definition's and `reason` says why.
|
|
137
|
+
*/
|
|
138
|
+
export interface DeployedVersion extends DeployedPrimitive {
|
|
139
|
+
/** The version the agent's definition names, when it differs from `version`. */
|
|
140
|
+
readonly authoredVersion?: string;
|
|
141
|
+
readonly reason?: VersionDerivationReason;
|
|
142
|
+
/** `true`: this deploy registered `version`; `false`: an earlier deploy did. */
|
|
143
|
+
readonly newVersion?: boolean;
|
|
144
|
+
/** For `pins-changed`: the pins that differ from `authoredVersion`'s. */
|
|
145
|
+
readonly pinChanges?: readonly PinChange[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** An agent a deployment shipped (`DeployedVersion`). */
|
|
149
|
+
export type DeployedAgent = DeployedVersion;
|
|
150
|
+
|
|
151
|
+
/** A flow a deployment shipped (`DeployedVersion`). */
|
|
152
|
+
export type DeployedFlow = DeployedVersion;
|
|
153
|
+
|
|
130
154
|
export interface DeploymentContents {
|
|
131
155
|
readonly tools: readonly DeployedPrimitive[];
|
|
132
156
|
readonly guardrails: readonly DeployedPrimitive[];
|
|
133
|
-
readonly agents: readonly
|
|
134
|
-
readonly flows: readonly
|
|
157
|
+
readonly agents: readonly DeployedAgent[];
|
|
158
|
+
readonly flows: readonly DeployedFlow[];
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
export interface DeploymentPrimitiveCounts {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type Agent,
|
|
6
|
+
type AgentId,
|
|
7
|
+
type AgentPins,
|
|
8
|
+
MODEL_SETTINGS_SCHEMA,
|
|
9
|
+
pinsDigest,
|
|
10
|
+
settingsSchemaIssues,
|
|
11
|
+
} from '@kindgi/agents';
|
|
12
|
+
import type { TupleEnqueueHook } from '@kindgi/authz';
|
|
13
|
+
import { latestVersion, nextVersion } from '@kindgi/tools';
|
|
14
|
+
import type { ProjectId, Semver, TenantId } from '@kindgi/types';
|
|
15
|
+
|
|
16
|
+
import type { AgentRegistryBinding, AgentVersionRecord } from './agent-binding.js';
|
|
17
|
+
import { activeAgentVersions } from './agent-pins.js';
|
|
18
|
+
import type { BlockRegistryBinding } from './block-binding.js';
|
|
19
|
+
|
|
20
|
+
/** Which data-block pins to swap, by kind then block id → exact version. */
|
|
21
|
+
export interface PinSwaps {
|
|
22
|
+
readonly prompts?: Readonly<Record<string, string>>;
|
|
23
|
+
readonly settings?: Readonly<Record<string, string>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DeriveAgentVersionInput {
|
|
27
|
+
readonly agents: AgentRegistryBinding;
|
|
28
|
+
readonly blocks: BlockRegistryBinding | undefined;
|
|
29
|
+
readonly tenantId: TenantId;
|
|
30
|
+
readonly agentId: AgentId;
|
|
31
|
+
/** The version to derive from: it must be pinned. */
|
|
32
|
+
readonly from: string;
|
|
33
|
+
readonly swaps: PinSwaps;
|
|
34
|
+
/** A short label for the new version. */
|
|
35
|
+
readonly label?: string;
|
|
36
|
+
/** Who derived it (`user:<id>`). */
|
|
37
|
+
readonly by?: string;
|
|
38
|
+
/** The project, when the store doesn't record one on the version. */
|
|
39
|
+
readonly projectId?: ProjectId;
|
|
40
|
+
/** The authorization tuples for the new version, in the project it lands in. */
|
|
41
|
+
readonly tuplesFor: (projectId: ProjectId) => TupleEnqueueHook;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SwapIssue {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DeriveAgentVersionOutcome =
|
|
50
|
+
| { readonly kind: 'ok'; readonly agent: Agent }
|
|
51
|
+
| { readonly kind: 'not-found' }
|
|
52
|
+
| { readonly kind: 'unpinned' }
|
|
53
|
+
| { readonly kind: 'invalid'; readonly issues: readonly SwapIssue[] }
|
|
54
|
+
| { readonly kind: 'no-project' }
|
|
55
|
+
| { readonly kind: 'project-not-found'; readonly projectId: ProjectId };
|
|
56
|
+
|
|
57
|
+
/** How many numbers after the highest a derive tries before it gives up. */
|
|
58
|
+
const MAX_TRIES = 100;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derive a new agent version from a pinned one with some data-block
|
|
62
|
+
* pins swapped: an expert's edit (a new prompt or settings version)
|
|
63
|
+
* reaching an agent without a code change. The new version is the old
|
|
64
|
+
* one's bag with those pins swapped (`derivedFrom: { version, reason:
|
|
65
|
+
* 'edited', label, by }`), numbered the next free patch after the
|
|
66
|
+
* agent's highest version (versions never change).
|
|
67
|
+
*
|
|
68
|
+
* Only data-block pins swap: a swap must name a block the version
|
|
69
|
+
* already references (adding one is a code change), at a published,
|
|
70
|
+
* active version of the right kind (and, for the model-settings block,
|
|
71
|
+
* model settings). Tool pins come from code.
|
|
72
|
+
*/
|
|
73
|
+
export async function deriveAgentVersion(
|
|
74
|
+
input: DeriveAgentVersionInput,
|
|
75
|
+
): Promise<DeriveAgentVersionOutcome> {
|
|
76
|
+
const { agents, tenantId, agentId } = input;
|
|
77
|
+
const from = await agents.getVersion({ tenantId, agentId, version: input.from as Semver });
|
|
78
|
+
if (from === null) return { kind: 'not-found' };
|
|
79
|
+
if (from.pins === undefined) return { kind: 'unpinned' };
|
|
80
|
+
|
|
81
|
+
const issues = await swapIssues(input, from, from.pins);
|
|
82
|
+
if (issues.length > 0) return { kind: 'invalid', issues };
|
|
83
|
+
const pins: AgentPins = {
|
|
84
|
+
tools: from.pins.tools,
|
|
85
|
+
prompts: { ...from.pins.prompts, ...(input.swaps.prompts ?? {}) },
|
|
86
|
+
settings: { ...from.pins.settings, ...(input.swaps.settings ?? {}) },
|
|
87
|
+
};
|
|
88
|
+
const digest = pinsDigest(pins);
|
|
89
|
+
if (digest === from.pinsDigest) {
|
|
90
|
+
return { kind: 'invalid', issues: [{ path: '/pins', message: 'no pin changes' }] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const projectId = from.projectId ?? input.projectId;
|
|
94
|
+
if (projectId === undefined) return { kind: 'no-project' };
|
|
95
|
+
const { unregisteredAt: _u, projectId: _p, derivedFrom: _d, ...definition } = from;
|
|
96
|
+
return publishNextFree(input, projectId, {
|
|
97
|
+
...definition,
|
|
98
|
+
pins,
|
|
99
|
+
pinsDigest: digest,
|
|
100
|
+
derivedFrom: {
|
|
101
|
+
version: from.version,
|
|
102
|
+
reason: 'edited',
|
|
103
|
+
...(input.label !== undefined && { label: input.label }),
|
|
104
|
+
...(input.by !== undefined && { by: input.by }),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Publish the derived version under the first free patch after the agent's highest. */
|
|
110
|
+
async function publishNextFree(
|
|
111
|
+
input: DeriveAgentVersionInput,
|
|
112
|
+
projectId: ProjectId,
|
|
113
|
+
derived: Agent,
|
|
114
|
+
): Promise<DeriveAgentVersionOutcome> {
|
|
115
|
+
const { agents, tenantId, agentId } = input;
|
|
116
|
+
const active = await activeAgentVersions(agents, tenantId, agentId);
|
|
117
|
+
const highest =
|
|
118
|
+
latestVersion(active.map((a) => a.version as unknown as string)) ??
|
|
119
|
+
(derived.version as unknown as string);
|
|
120
|
+
let candidate = nextVersion(highest);
|
|
121
|
+
for (let tries = 0; candidate !== undefined && tries < MAX_TRIES; tries++) {
|
|
122
|
+
const agent: Agent = { ...derived, version: candidate as Semver };
|
|
123
|
+
const outcome = await agents.publish({
|
|
124
|
+
tenantId,
|
|
125
|
+
projectId,
|
|
126
|
+
agent,
|
|
127
|
+
enqueueTuples: input.tuplesFor(projectId),
|
|
128
|
+
});
|
|
129
|
+
if (outcome.kind === 'ok') return { kind: 'ok', agent };
|
|
130
|
+
if (outcome.kind === 'project-not-found') return { kind: 'project-not-found', projectId };
|
|
131
|
+
candidate = nextVersion(candidate);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`agent "${agentId as unknown as string}": no free version after ${highest}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** What's wrong with the swaps, against the version's references and the block store. */
|
|
137
|
+
async function swapIssues(
|
|
138
|
+
input: DeriveAgentVersionInput,
|
|
139
|
+
from: AgentVersionRecord,
|
|
140
|
+
pins: AgentPins,
|
|
141
|
+
): Promise<SwapIssue[]> {
|
|
142
|
+
const issues: SwapIssue[] = [];
|
|
143
|
+
const swaps = [
|
|
144
|
+
...Object.entries(input.swaps.prompts ?? {}).map(([id, v]) => ['prompts', id, v] as const),
|
|
145
|
+
...Object.entries(input.swaps.settings ?? {}).map(([id, v]) => ['settings', id, v] as const),
|
|
146
|
+
];
|
|
147
|
+
if (swaps.length === 0)
|
|
148
|
+
return [{ path: '/pins', message: 'name at least one prompt or settings pin to swap' }];
|
|
149
|
+
for (const [kind, id, version] of swaps) {
|
|
150
|
+
const path = `/pins/${kind}/${id}`;
|
|
151
|
+
if (pins[kind][id] === undefined) {
|
|
152
|
+
issues.push({
|
|
153
|
+
path,
|
|
154
|
+
message: `agent version ${from.version} doesn't reference ${kind === 'prompts' ? 'prompt' : 'settings'} block "${id}"; adding a block is a code change`,
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const problem = await blockProblem(input, from, kind, id, version);
|
|
159
|
+
if (problem !== undefined) issues.push({ path, message: problem });
|
|
160
|
+
}
|
|
161
|
+
return issues;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function blockProblem(
|
|
165
|
+
input: DeriveAgentVersionInput,
|
|
166
|
+
from: AgentVersionRecord,
|
|
167
|
+
kind: 'prompts' | 'settings',
|
|
168
|
+
id: string,
|
|
169
|
+
version: string,
|
|
170
|
+
): Promise<string | undefined> {
|
|
171
|
+
if (input.blocks === undefined) return 'this runtime serves no data blocks';
|
|
172
|
+
const block = await input.blocks.getVersion({ tenantId: input.tenantId, blockId: id, version });
|
|
173
|
+
const what = `block "${id}" version ${version}`;
|
|
174
|
+
if (block === null) return `${what} isn't published`;
|
|
175
|
+
if (block.unregisteredAt !== undefined) return `${what} is unregistered`;
|
|
176
|
+
const wanted = kind === 'prompts' ? 'prompt' : 'settings';
|
|
177
|
+
if (block.kind !== wanted) return `${what} is a ${block.kind} block, not ${wanted}`;
|
|
178
|
+
if (block.kind === 'settings' && from.modelSettings?.id === id) {
|
|
179
|
+
const issues = settingsSchemaIssues(block.content.values, MODEL_SETTINGS_SCHEMA);
|
|
180
|
+
if (issues.length > 0) {
|
|
181
|
+
return `${what} isn't model settings: ${issues.map((i) => `${i.path} ${i.message}`).join('; ')}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The first version after `after` that no version of the agent holds,
|
|
189
|
+
* active or unregistered (versions never change): what a refused
|
|
190
|
+
* publish of a taken number suggests, by the deploy rule's numbering.
|
|
191
|
+
* `undefined` when none is free within the tries.
|
|
192
|
+
*/
|
|
193
|
+
export async function nextFreeAgentVersion(
|
|
194
|
+
agents: AgentRegistryBinding,
|
|
195
|
+
tenantId: TenantId,
|
|
196
|
+
agentId: AgentId,
|
|
197
|
+
after: string,
|
|
198
|
+
): Promise<string | undefined> {
|
|
199
|
+
let candidate = nextVersion(after);
|
|
200
|
+
for (let tries = 0; candidate !== undefined && tries < MAX_TRIES; tries++) {
|
|
201
|
+
const held = await agents.getVersion({ tenantId, agentId, version: candidate as Semver });
|
|
202
|
+
if (held === null) return candidate;
|
|
203
|
+
candidate = nextVersion(candidate);
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface WireError {
|
|
|
31
31
|
export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
|
|
32
32
|
// 400 — bad request
|
|
33
33
|
'validation-failed': 400,
|
|
34
|
+
'kind-not-applied': 400,
|
|
34
35
|
'unknown-field': 400,
|
|
35
36
|
'bad-input': 400,
|
|
36
37
|
'unresolved-tool': 400,
|
|
@@ -160,10 +161,22 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
|
|
|
160
161
|
'policy-not-found': 404,
|
|
161
162
|
'policy-already-registered': 409,
|
|
162
163
|
// Admin plane — eval suites.
|
|
164
|
+
'block-not-found': 404,
|
|
165
|
+
'block-already-registered': 409,
|
|
166
|
+
'block-project-mismatch': 409,
|
|
163
167
|
'eval-suite-not-found': 404,
|
|
164
168
|
'eval-suite-already-registered': 409,
|
|
165
169
|
// Admin plane — eval-run dispatch.
|
|
166
170
|
'eval-run-not-found': 404,
|
|
171
|
+
// Judgments (yes/no on a run's output items) and judge classes.
|
|
172
|
+
'judgment-not-found': 404,
|
|
173
|
+
'judge-class-not-found': 404,
|
|
174
|
+
'judge-class-name-taken': 409,
|
|
175
|
+
'judge-class-not-applicable': 400,
|
|
176
|
+
'run-not-finished': 409,
|
|
177
|
+
'item-not-found': 400,
|
|
178
|
+
// The judgment binding can't list judged runs, so no test sets from judgments.
|
|
179
|
+
'test-sets-not-supported': 501,
|
|
167
180
|
'eval-run-already-terminal': 409,
|
|
168
181
|
'dispatcher-not-registered': 422,
|
|
169
182
|
'dispatcher-input-invalid': 400,
|
|
@@ -215,6 +228,10 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
|
|
|
215
228
|
'project-not-found': 404,
|
|
216
229
|
'team-membership-not-found': 404,
|
|
217
230
|
'project-membership-not-found': 404,
|
|
231
|
+
// A slug another org, team or project in the tenant already has; a
|
|
232
|
+
// second Default project.
|
|
233
|
+
'slug-conflict': 409,
|
|
234
|
+
'project-default-already-exists': 409,
|
|
218
235
|
'tenant-not-found': 404,
|
|
219
236
|
'tenant-config-not-found': 404,
|
|
220
237
|
'tenant-config-revision-conflict': 409,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { Cursor, TenantId } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
import type { JudgedItem, JudgedRunContext, JudgedSubject } from './judgment-binding.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Caller-plugged storage for the cases of an eval-suite version, kept
|
|
10
|
+
* apart from the version's `spec` because a set can hold hundreds of
|
|
11
|
+
* cases. A `judged` suite version's cases are built from judgments: each
|
|
12
|
+
* is a copy of one judged run (its input, what it read, the output that
|
|
13
|
+
* was judged) with the judgments of its items summed up. Copies, so a
|
|
14
|
+
* test set stays the same when judgments are removed or runs purged.
|
|
15
|
+
*
|
|
16
|
+
* Cases are written once, when the version is published, and removed
|
|
17
|
+
* with it.
|
|
18
|
+
*/
|
|
19
|
+
export interface EvalCaseStoreBinding {
|
|
20
|
+
/** Store a version's cases. Called once, right after the version is published. */
|
|
21
|
+
putCases(input: EvalCasePutInput): Promise<void>;
|
|
22
|
+
/** A version's cases, in the order they were stored, cursor-paginated. */
|
|
23
|
+
listCases(input: EvalCaseListInput): Promise<EvalCasePage>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The judgments of one item of a case's output, summed up. */
|
|
27
|
+
export interface JudgedItemSummary extends JudgedItem {
|
|
28
|
+
readonly yes: number;
|
|
29
|
+
readonly no: number;
|
|
30
|
+
/** The weight behind "yes" (an unclassified judgment counts 1). */
|
|
31
|
+
readonly yesWeight: number;
|
|
32
|
+
/** The weight behind all judgments of the item. */
|
|
33
|
+
readonly totalWeight: number;
|
|
34
|
+
/** The reasons given, newest first. */
|
|
35
|
+
readonly reasons: readonly { readonly verdict: 'yes' | 'no'; readonly reason: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One case of a `judged` suite: a copy of a judged run and what people said about it. */
|
|
39
|
+
export interface JudgedEvalCase {
|
|
40
|
+
/** The judged run's id. */
|
|
41
|
+
readonly caseId: string;
|
|
42
|
+
/** What ran: the agent or flow at a version (the baseline when comparing to the recording). */
|
|
43
|
+
readonly subject: JudgedSubject;
|
|
44
|
+
readonly input: unknown;
|
|
45
|
+
/** What the turn read besides its input; absent when it wasn't captured. */
|
|
46
|
+
readonly context?: JudgedRunContext;
|
|
47
|
+
/** The output that was judged. */
|
|
48
|
+
readonly output: unknown;
|
|
49
|
+
readonly items: readonly JudgedItemSummary[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface EvalCasePutInput {
|
|
53
|
+
readonly tenantId: TenantId;
|
|
54
|
+
readonly suiteId: string;
|
|
55
|
+
readonly version: string;
|
|
56
|
+
readonly cases: readonly JudgedEvalCase[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EvalCaseListInput {
|
|
60
|
+
readonly tenantId: TenantId;
|
|
61
|
+
readonly suiteId: string;
|
|
62
|
+
readonly version: string;
|
|
63
|
+
readonly cursor?: Cursor;
|
|
64
|
+
readonly limit: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface EvalCasePage {
|
|
68
|
+
readonly data: readonly JudgedEvalCase[];
|
|
69
|
+
readonly hasMore: boolean;
|
|
70
|
+
readonly nextCursor?: Cursor;
|
|
71
|
+
}
|
package/src/eval-run-binding.ts
CHANGED
|
@@ -105,6 +105,8 @@ export interface EvalRun {
|
|
|
105
105
|
readonly result?: Readonly<Record<string, unknown>>;
|
|
106
106
|
readonly error?: string;
|
|
107
107
|
readonly correlationId?: string;
|
|
108
|
+
/** A comparison eval run's baseline, reads and repetitions. */
|
|
109
|
+
readonly comparison?: EvalComparison;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export interface EvalRunFilter {
|
|
@@ -116,6 +118,35 @@ export interface EvalRunFilter {
|
|
|
116
118
|
readonly to?: Timestamp;
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* What a comparison eval run compares the candidate against:
|
|
123
|
+
* - `'recorded'`: the output each case recorded (what was judged);
|
|
124
|
+
* - `{ agentId, version }`: that version, replayed under the same rules;
|
|
125
|
+
* - `{ live }`: the version live in a scope (a project, segments).
|
|
126
|
+
*/
|
|
127
|
+
export type EvalBaseline =
|
|
128
|
+
| 'recorded'
|
|
129
|
+
| { readonly agentId: AgentId; readonly version: Semver }
|
|
130
|
+
| {
|
|
131
|
+
readonly live: {
|
|
132
|
+
readonly projectId?: ProjectId;
|
|
133
|
+
readonly segments?: Readonly<Record<string, string>>;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Whether replayed reads use the past run's results when it has them, or run live. */
|
|
138
|
+
export type EvalReads = 'recorded' | 'live';
|
|
139
|
+
|
|
140
|
+
/** How a comparison eval run runs its cases (absent: the run isn't a comparison). */
|
|
141
|
+
export interface EvalComparison {
|
|
142
|
+
readonly baseline: EvalBaseline;
|
|
143
|
+
readonly reads: EvalReads;
|
|
144
|
+
/** How many times each case runs (1–10); with more than 1, the summary shows the spread. */
|
|
145
|
+
readonly repetitions: number;
|
|
146
|
+
/** How many ranked items `weightedPrecisionAtK` looks at (1–100). */
|
|
147
|
+
readonly k: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
119
150
|
export interface EvalRunStartInput {
|
|
120
151
|
readonly tenantId: TenantId;
|
|
121
152
|
/**
|
|
@@ -130,6 +161,8 @@ export interface EvalRunStartInput {
|
|
|
130
161
|
readonly flowRef?: FlowRef;
|
|
131
162
|
readonly dryRun?: boolean;
|
|
132
163
|
readonly correlationId?: string;
|
|
164
|
+
/** Set for a comparison eval run (a `judged` suite): its baseline, reads and repetitions. */
|
|
165
|
+
readonly comparison?: EvalComparison;
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
/**
|