@kindgi/api 0.1.2 → 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 +22 -3
- 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 +100 -5
- package/dist/cost-binding.d.ts.map +1 -1
- package/dist/cost-binding.js +10 -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/hitl-binding.d.ts +20 -5
- package/dist/hitl-binding.d.ts.map +1 -1
- package/dist/index.d.ts +19 -8
- 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/middleware/auth.d.ts +3 -2
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +5 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +8 -1
- package/dist/middleware/idempotency.js.map +1 -1
- 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 +543 -24
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +58 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1058 -27
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/provenance-binding.d.ts +27 -1
- package/dist/provenance-binding.d.ts.map +1 -1
- package/dist/provenance-binding.js.map +1 -1
- package/dist/provider-binding.d.ts +12 -7
- package/dist/provider-binding.d.ts.map +1 -1
- package/dist/reviewer-binding.d.ts +10 -2
- package/dist/reviewer-binding.d.ts.map +1 -1
- package/dist/reviewer-role.d.ts +13 -0
- package/dist/reviewer-role.d.ts.map +1 -0
- package/dist/reviewer-role.js +27 -0
- package/dist/reviewer-role.js.map +1 -0
- 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/approvals.d.ts +5 -4
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +52 -16
- package/dist/routes/approvals.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/conversations.d.ts +7 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +41 -1
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +142 -11
- 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/identity.d.ts +7 -0
- package/dist/routes/identity.d.ts.map +1 -1
- package/dist/routes/identity.js +3 -2
- package/dist/routes/identity.js.map +1 -1
- 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/provenance.d.ts.map +1 -1
- package/dist/routes/provenance.js +32 -1
- package/dist/routes/provenance.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.d.ts +0 -7
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +65 -20
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/scope-params.d.ts +16 -1
- package/dist/routes/scope-params.d.ts.map +1 -1
- package/dist/routes/scope-params.js +28 -0
- package/dist/routes/scope-params.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/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.d.ts +11 -0
- package/dist/webhook-endpoint-binding.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.js.map +1 -1
- package/openapi.json +13316 -9299
- package/package.json +21 -21
- package/src/agent-binding.ts +19 -4
- package/src/agent-pins.ts +147 -0
- package/src/app.ts +76 -3
- package/src/block-binding.ts +137 -0
- package/src/block-pins.ts +148 -0
- package/src/cost-binding.ts +116 -5
- 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/hitl-binding.ts +20 -4
- package/src/index.ts +88 -2
- package/src/judged-dispatcher.ts +507 -0
- package/src/judged-items.ts +263 -0
- package/src/judgment-binding.ts +349 -0
- package/src/middleware/auth.ts +3 -2
- package/src/middleware/idempotency.ts +7 -1
- package/src/openapi/generate.ts +7 -1
- package/src/openapi/operations.ts +615 -24
- package/src/openapi/schemas.ts +1157 -22
- package/src/provenance-binding.ts +42 -1
- package/src/provider-binding.ts +12 -7
- package/src/reviewer-binding.ts +10 -2
- package/src/reviewer-role.ts +35 -0
- package/src/routes/agents.ts +243 -19
- package/src/routes/approvals.ts +70 -19
- package/src/routes/blocks.ts +362 -0
- package/src/routes/conversations.ts +57 -1
- package/src/routes/cost.ts +159 -21
- 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/identity.ts +10 -2
- 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/provenance.ts +48 -1
- package/src/routes/providers.ts +5 -0
- package/src/routes/runs.ts +79 -22
- package/src/routes/scope-params.ts +35 -1
- package/src/routes/teams.ts +96 -90
- package/src/types.ts +4 -3
- package/src/webhook-endpoint-binding.ts +11 -0
|
@@ -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
|
/**
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { ReplayTurnReport } from '@kindgi/agents';
|
|
7
|
+
import type { RunReplayRef } from '@kindgi/runtime';
|
|
8
|
+
import type { ProjectId, RunId, TenantId, Timestamp } from '@kindgi/types';
|
|
7
9
|
|
|
8
10
|
import type {
|
|
9
11
|
AgentRef,
|
|
12
|
+
EvalComparison,
|
|
10
13
|
EvalRun,
|
|
11
14
|
EvalRunBinding,
|
|
12
15
|
EvalRunCancelInput,
|
|
@@ -50,6 +53,16 @@ export interface EvalRunSubjectInvokeInput {
|
|
|
50
53
|
readonly input: unknown;
|
|
51
54
|
readonly dryRun: boolean;
|
|
52
55
|
readonly abortSignal: AbortSignal;
|
|
56
|
+
/** The eval run's project: where the subject runs. */
|
|
57
|
+
readonly projectId?: ProjectId;
|
|
58
|
+
/**
|
|
59
|
+
* Set when the subject re-runs a past run (a comparison eval run): the
|
|
60
|
+
* run is a replay of `of` for the eval run, under the replay rules
|
|
61
|
+
* (`InvokeAgentInput.replay`).
|
|
62
|
+
*/
|
|
63
|
+
readonly replay?: RunReplayRef;
|
|
64
|
+
/** The conversation before the past turn, oldest first: a replayed turn starts from it. */
|
|
65
|
+
readonly history?: readonly unknown[];
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
export interface EvalRunSubjectInvokeOutcome {
|
|
@@ -57,6 +70,22 @@ export interface EvalRunSubjectInvokeOutcome {
|
|
|
57
70
|
readonly error?: string;
|
|
58
71
|
readonly costUsd?: number;
|
|
59
72
|
readonly durationMs?: number;
|
|
73
|
+
/** The run the subject ran in. */
|
|
74
|
+
readonly runId?: RunId;
|
|
75
|
+
/** A replay's report: each tool call and what happened to it (`AgentTurnResult.replay`). */
|
|
76
|
+
readonly replay?: ReplayTurnReport;
|
|
77
|
+
/** The provider and model that answered. */
|
|
78
|
+
readonly provider?: { readonly id: string; readonly model: string };
|
|
79
|
+
/**
|
|
80
|
+
* Set when a replayed flow stopped at a tool call the replay refused (a
|
|
81
|
+
* write the past run didn't make): no output, and what it would have
|
|
82
|
+
* done. Not an error: the replay did what it's meant to.
|
|
83
|
+
*/
|
|
84
|
+
readonly stopped?: {
|
|
85
|
+
readonly toolId: string;
|
|
86
|
+
readonly arguments: unknown;
|
|
87
|
+
readonly reason?: string;
|
|
88
|
+
};
|
|
60
89
|
}
|
|
61
90
|
|
|
62
91
|
/**
|
|
@@ -100,6 +129,10 @@ export interface DispatchContext {
|
|
|
100
129
|
readonly dryRun: boolean;
|
|
101
130
|
readonly abortSignal: AbortSignal;
|
|
102
131
|
readonly subject: EvalSubjectInvoker;
|
|
132
|
+
/** The eval run's project. */
|
|
133
|
+
readonly projectId?: ProjectId;
|
|
134
|
+
/** A comparison eval run's baseline, reads and repetitions. */
|
|
135
|
+
readonly comparison?: EvalComparison;
|
|
103
136
|
onProgress(perCase: Readonly<Record<string, unknown>>): void;
|
|
104
137
|
}
|
|
105
138
|
|
|
@@ -113,6 +146,7 @@ export interface EvalRunDispatcher {
|
|
|
113
146
|
validate?(
|
|
114
147
|
suite: EvalSuite,
|
|
115
148
|
target: AgentRef | FlowRef,
|
|
149
|
+
comparison?: EvalComparison,
|
|
116
150
|
): { kind: 'ok' } | { kind: 'err'; message: string };
|
|
117
151
|
dispatch(ctx: DispatchContext): Promise<DispatchResult>;
|
|
118
152
|
}
|
|
@@ -311,26 +345,13 @@ export function createInProcessEvalRunBinding(
|
|
|
311
345
|
};
|
|
312
346
|
}
|
|
313
347
|
if (dispatcher.validate !== undefined) {
|
|
314
|
-
const v = dispatcher.validate(suite, target);
|
|
348
|
+
const v = dispatcher.validate(suite, target, input.comparison);
|
|
315
349
|
if (v.kind === 'err') {
|
|
316
350
|
return { kind: 'dispatcher-input-invalid', message: v.message };
|
|
317
351
|
}
|
|
318
352
|
}
|
|
319
353
|
const runId = randomUUID() as unknown as RunId;
|
|
320
|
-
const
|
|
321
|
-
const record: EvalRun = {
|
|
322
|
-
runId,
|
|
323
|
-
tenantId: input.tenantId,
|
|
324
|
-
suiteId: suite.id,
|
|
325
|
-
suiteVersion: suite.version,
|
|
326
|
-
kind: suite.kind,
|
|
327
|
-
...(input.agentRef !== undefined && { agentRef: input.agentRef }),
|
|
328
|
-
...(input.flowRef !== undefined && { flowRef: input.flowRef }),
|
|
329
|
-
status: 'running' as EvalRunStatus,
|
|
330
|
-
dryRun: input.dryRun === true,
|
|
331
|
-
startedAt,
|
|
332
|
-
...(input.correlationId !== undefined && { correlationId: input.correlationId }),
|
|
333
|
-
};
|
|
354
|
+
const record = newRunRecord(input, suite, runId, now());
|
|
334
355
|
setRun(record);
|
|
335
356
|
const controller = new AbortController();
|
|
336
357
|
controllers.set(runId as unknown as string, controller);
|
|
@@ -349,6 +370,8 @@ export function createInProcessEvalRunBinding(
|
|
|
349
370
|
dryRun: input.dryRun === true,
|
|
350
371
|
abortSignal: controller.signal,
|
|
351
372
|
subject: options.subject,
|
|
373
|
+
projectId: input.projectId,
|
|
374
|
+
...(input.comparison !== undefined && { comparison: input.comparison }),
|
|
352
375
|
onProgress: (perCaseEntry: Readonly<Record<string, unknown>>) => {
|
|
353
376
|
const current = runs.get(runId as unknown as string);
|
|
354
377
|
if (current === undefined) return;
|
|
@@ -489,6 +512,24 @@ export function createInProcessEvalRunBinding(
|
|
|
489
512
|
};
|
|
490
513
|
}
|
|
491
514
|
|
|
515
|
+
/** A started run's row: `running`, with what it runs and how. */
|
|
516
|
+
function newRunRecord(input: EvalRunStartInput, suite: EvalSuite, runId: RunId, at: Date): EvalRun {
|
|
517
|
+
return {
|
|
518
|
+
runId,
|
|
519
|
+
tenantId: input.tenantId,
|
|
520
|
+
suiteId: suite.id,
|
|
521
|
+
suiteVersion: suite.version,
|
|
522
|
+
kind: suite.kind,
|
|
523
|
+
...(input.agentRef !== undefined && { agentRef: input.agentRef }),
|
|
524
|
+
...(input.flowRef !== undefined && { flowRef: input.flowRef }),
|
|
525
|
+
status: 'running',
|
|
526
|
+
dryRun: input.dryRun === true,
|
|
527
|
+
startedAt: at.toISOString() as unknown as Timestamp,
|
|
528
|
+
...(input.correlationId !== undefined && { correlationId: input.correlationId }),
|
|
529
|
+
...(input.comparison !== undefined && { comparison: input.comparison }),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
492
533
|
function isTerminal(status: EvalRunStatus): boolean {
|
|
493
534
|
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
494
535
|
}
|
|
@@ -100,6 +100,8 @@ export const EVAL_KINDS = [
|
|
|
100
100
|
'human-review',
|
|
101
101
|
'benchmark',
|
|
102
102
|
'custom',
|
|
103
|
+
/** Built from judgments of past runs: cases are copies of judged runs (`EvalCaseStoreBinding`). */
|
|
104
|
+
'judged',
|
|
103
105
|
] as const;
|
|
104
106
|
export type EvalKind = (typeof EVAL_KINDS)[number];
|
|
105
107
|
|
package/src/flow-binding.ts
CHANGED
|
@@ -39,11 +39,12 @@ export interface FlowRegistryBinding {
|
|
|
39
39
|
*/
|
|
40
40
|
get(input: FlowGetInput): Promise<Flow | null>;
|
|
41
41
|
/**
|
|
42
|
-
* Specific `(flowId, version)` lookup, or `null` if unknown.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Specific `(flowId, version)` lookup, or `null` if unknown. Returns
|
|
43
|
+
* unregistered (tombstoned) versions too, with `unregisteredAt` set:
|
|
44
|
+
* a resumed run and provenance read them, while a new run that names
|
|
45
|
+
* one is refused.
|
|
45
46
|
*/
|
|
46
|
-
getVersion(input: FlowGetVersionInput): Promise<
|
|
47
|
+
getVersion(input: FlowGetVersionInput): Promise<FlowVersionRecord | null>;
|
|
47
48
|
/**
|
|
48
49
|
* Head-row existence check. Lets `GET /v1/flows/{id}` distinguish
|
|
49
50
|
* `410 gone` (identity exists, no active version) from `404 not-
|
|
@@ -79,6 +80,12 @@ export interface FlowRegistryBinding {
|
|
|
79
80
|
reinstateVersion(input: FlowReinstateVersionInput): Promise<FlowReinstateVersionOutcome>;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/** A flow version as `getVersion` reads it: `unregisteredAt` is set when it's unregistered. */
|
|
84
|
+
export type FlowVersionRecord = Flow & {
|
|
85
|
+
/** ISO-8601; present only on an unregistered version. */
|
|
86
|
+
readonly unregisteredAt?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
82
89
|
export interface FlowListInput {
|
|
83
90
|
readonly tenantId: TenantId;
|
|
84
91
|
readonly limit: number;
|
package/src/flow-pins.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { AgentId } from '@kindgi/agents';
|
|
5
|
+
import type { TupleEnqueueHook } from '@kindgi/authz';
|
|
6
|
+
import { type Flow, type FlowPins, flowPinsDigest, flowRefs } from '@kindgi/flow';
|
|
7
|
+
import { latestVersion } from '@kindgi/tools';
|
|
8
|
+
import type { Cursor, FlowId, ProjectId, TenantId, ToolId } from '@kindgi/types';
|
|
9
|
+
|
|
10
|
+
import type { AgentRegistryBinding } from './agent-binding.js';
|
|
11
|
+
import { type UnpinnableRef, activeToolVersions } from './agent-pins.js';
|
|
12
|
+
import { type DeployedVersionOutcome, deployVersion } from './deploy-versions.js';
|
|
13
|
+
import type { FlowRegistryBinding } from './flow-binding.js';
|
|
14
|
+
import type { ToolRegistryBinding } from './tool-binding.js';
|
|
15
|
+
|
|
16
|
+
export type FlowPinsOutcome =
|
|
17
|
+
| { readonly kind: 'ok'; readonly pins: FlowPins }
|
|
18
|
+
| { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pin a flow version when it's published: each tool it runs, and each
|
|
22
|
+
* agent it runs at no named version, to its latest active version now
|
|
23
|
+
* (what a run would have bound), so every run of the version uses those.
|
|
24
|
+
* A tool or agent with no published version makes the flow unpinnable:
|
|
25
|
+
* the caller refuses the publish, naming each, rather than store a
|
|
26
|
+
* partly pinned version.
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveFlowPins(
|
|
29
|
+
tools: ToolRegistryBinding,
|
|
30
|
+
agents: AgentRegistryBinding,
|
|
31
|
+
tenantId: TenantId,
|
|
32
|
+
flow: Flow,
|
|
33
|
+
): Promise<FlowPinsOutcome> {
|
|
34
|
+
const refs = flowRefs(flow);
|
|
35
|
+
const pinned: { tools: Record<string, string>; agents: Record<string, string> } = {
|
|
36
|
+
tools: {},
|
|
37
|
+
agents: {},
|
|
38
|
+
};
|
|
39
|
+
const issues: UnpinnableRef[] = [];
|
|
40
|
+
for (const id of refs.tools) {
|
|
41
|
+
const latest = latestVersion(await activeToolVersions(tools, tenantId, id as ToolId));
|
|
42
|
+
if (latest !== undefined) pinned.tools[id] = latest;
|
|
43
|
+
else
|
|
44
|
+
issues.push({
|
|
45
|
+
path: '/nodes',
|
|
46
|
+
message: `tool "${id}" has no published version; publish the tool first`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
for (const id of refs.agents) {
|
|
50
|
+
const latest = await agents.get({ tenantId, agentId: id as AgentId });
|
|
51
|
+
if (latest !== null) pinned.agents[id] = latest.version as unknown as string;
|
|
52
|
+
else
|
|
53
|
+
issues.push({
|
|
54
|
+
path: '/nodes',
|
|
55
|
+
message: `agent "${id}" has no published version; publish the agent first`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (issues.length > 0) return { kind: 'unpinnable', issues };
|
|
59
|
+
return { kind: 'ok', pins: pinned };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PublishDeployedFlowInput {
|
|
63
|
+
readonly flows: FlowRegistryBinding;
|
|
64
|
+
readonly tenantId: TenantId;
|
|
65
|
+
readonly projectId: ProjectId;
|
|
66
|
+
/** The flow as the pack defines it, with no pins. */
|
|
67
|
+
readonly flow: Flow;
|
|
68
|
+
readonly pins: FlowPins;
|
|
69
|
+
readonly enqueueTuples: TupleEnqueueHook;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Register a deployed flow with its pins, by the deploy rule (`deployVersion`). */
|
|
73
|
+
export async function publishDeployedFlow(
|
|
74
|
+
input: PublishDeployedFlowInput,
|
|
75
|
+
): Promise<DeployedVersionOutcome> {
|
|
76
|
+
const { flows, tenantId, projectId, flow, pins, enqueueTuples } = input;
|
|
77
|
+
return deployVersion<Flow>({
|
|
78
|
+
label: `flow "${flow.id as unknown as string}"`,
|
|
79
|
+
definition: flow,
|
|
80
|
+
pins,
|
|
81
|
+
pinsDigest: flowPinsDigest(pins),
|
|
82
|
+
existing: await allFlowVersions(flows, tenantId, flow.id),
|
|
83
|
+
publish: async (version) => {
|
|
84
|
+
const outcome = await flows.publish({ tenantId, projectId, flow: version, enqueueTuples });
|
|
85
|
+
if (outcome.kind === 'ok') return 'ok';
|
|
86
|
+
return outcome.kind === 'project-not-found' ? 'skipped' : 'taken';
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Page size for reading a flow's versions. */
|
|
92
|
+
const VERSIONS_PAGE = 200;
|
|
93
|
+
|
|
94
|
+
/** Every active version of a flow. */
|
|
95
|
+
async function allFlowVersions(
|
|
96
|
+
flows: FlowRegistryBinding,
|
|
97
|
+
tenantId: TenantId,
|
|
98
|
+
flowId: FlowId,
|
|
99
|
+
): Promise<readonly Flow[]> {
|
|
100
|
+
const versions: Flow[] = [];
|
|
101
|
+
let cursor: Cursor | undefined;
|
|
102
|
+
do {
|
|
103
|
+
const page = await flows.listVersions({
|
|
104
|
+
tenantId,
|
|
105
|
+
flowId,
|
|
106
|
+
limit: VERSIONS_PAGE,
|
|
107
|
+
...(cursor !== undefined && { cursor }),
|
|
108
|
+
});
|
|
109
|
+
versions.push(...page.data);
|
|
110
|
+
cursor = page.nextCursor;
|
|
111
|
+
} while (cursor !== undefined);
|
|
112
|
+
return versions;
|
|
113
|
+
}
|