@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
|
@@ -5,9 +5,10 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
7
|
|
|
8
|
-
import type { AgentId } from '@kindgi/agents';
|
|
8
|
+
import type { Agent, AgentId, AgentPins } from '@kindgi/agents';
|
|
9
9
|
import { tuplesForCreate } from '@kindgi/authz';
|
|
10
10
|
import { parsePublicKeyPem, verifyEd25519 } from '@kindgi/crypto';
|
|
11
|
+
import type { Flow, FlowPins } from '@kindgi/flow';
|
|
11
12
|
import { type Guardrail, validateGuardrailSpec } from '@kindgi/guardrails';
|
|
12
13
|
import type { ProjectBinding, Scope } from '@kindgi/platform';
|
|
13
14
|
import { validateToolManifest } from '@kindgi/tools';
|
|
@@ -16,13 +17,20 @@ import type {
|
|
|
16
17
|
FlowId,
|
|
17
18
|
GuardrailId,
|
|
18
19
|
ProjectId,
|
|
20
|
+
Semver,
|
|
19
21
|
SigningKeyId,
|
|
20
22
|
TenantId,
|
|
21
23
|
ToolId,
|
|
22
24
|
} from '@kindgi/types';
|
|
23
25
|
|
|
24
26
|
import type { AgentRegistryBinding } from '../agent-binding.js';
|
|
27
|
+
import { type UnpinnableRef, publishDeployedAgent, resolveAgentPins } from '../agent-pins.js';
|
|
28
|
+
import type { BlockRegistryBinding } from '../block-binding.js';
|
|
29
|
+
import type { DeployedVersionOutcome } from '../deploy-versions.js';
|
|
25
30
|
import type {
|
|
31
|
+
DeployedAgent,
|
|
32
|
+
DeployedFlow,
|
|
33
|
+
DeployedVersion,
|
|
26
34
|
Deployment,
|
|
27
35
|
DeploymentBinding,
|
|
28
36
|
DeploymentPrimitiveCounts,
|
|
@@ -30,6 +38,7 @@ import type {
|
|
|
30
38
|
} from '../deployment-binding.js';
|
|
31
39
|
import { statusFor, toWireError } from '../errors.js';
|
|
32
40
|
import type { FlowRegistryBinding } from '../flow-binding.js';
|
|
41
|
+
import { publishDeployedFlow, resolveFlowPins } from '../flow-pins.js';
|
|
33
42
|
import type { GuardrailRegistryBinding } from '../guardrail-binding.js';
|
|
34
43
|
import type { ImageRegistryBinding } from '../image-registry-binding.js';
|
|
35
44
|
import type { SecretBinding } from '../secrets-binding.js';
|
|
@@ -112,6 +121,8 @@ export interface DeploymentsRouterBindings {
|
|
|
112
121
|
readonly guardrailRegistry?: GuardrailRegistryBinding;
|
|
113
122
|
readonly agentRegistry?: AgentRegistryBinding;
|
|
114
123
|
readonly flowRegistry?: FlowRegistryBinding;
|
|
124
|
+
/** Data blocks: an agent's block references are pinned at deploy with its tools. */
|
|
125
|
+
readonly blockRegistry?: BlockRegistryBinding;
|
|
115
126
|
/**
|
|
116
127
|
* REQUIRED at runtime for `POST /v1/deployments/:deploymentId/secrets`.
|
|
117
128
|
* Optional at the type level so app compositions without a secrets
|
|
@@ -529,6 +540,10 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
|
|
|
529
540
|
}
|
|
530
541
|
|
|
531
542
|
const rolled: RollbackAction[] = [];
|
|
543
|
+
// The version each agent is registered under: its definition's, or
|
|
544
|
+
// the one a deploy registered in its place (`publishDeployedAgent`).
|
|
545
|
+
const deployedAgents: DeployedAgent[] = [];
|
|
546
|
+
const deployedFlows: DeployedFlow[] = [];
|
|
532
547
|
try {
|
|
533
548
|
if (bindings.toolRegistry !== undefined && defaultProjectId !== undefined) {
|
|
534
549
|
for (const tool of validated.tools) {
|
|
@@ -578,58 +593,50 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
|
|
|
578
593
|
}
|
|
579
594
|
}
|
|
580
595
|
if (bindings.agentRegistry !== undefined && defaultProjectId !== undefined) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
596
|
+
deployedAgents.push(
|
|
597
|
+
...(await registerAgents({
|
|
598
|
+
agents: bindings.agentRegistry,
|
|
599
|
+
tools: bindings.toolRegistry,
|
|
600
|
+
blocks: bindings.blockRegistry,
|
|
584
601
|
tenantId,
|
|
585
|
-
projectId:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
kind: 'agent',
|
|
593
|
-
id: agentId as AgentId,
|
|
594
|
-
tenantId,
|
|
595
|
-
projectId: projectIdForAgent,
|
|
596
|
-
}),
|
|
597
|
-
});
|
|
598
|
-
if (outcome.kind === 'ok') {
|
|
599
|
-
const agentId = outcome.agentId;
|
|
600
|
-
const version = outcome.version;
|
|
601
|
-
rolled.push(async () => {
|
|
602
|
-
await bindings.agentRegistry?.unregister({ tenantId, agentId, version });
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
602
|
+
projectId: defaultProjectId,
|
|
603
|
+
defined: validated.agents,
|
|
604
|
+
rolled,
|
|
605
|
+
})),
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
deployedAgents.push(...validated.agents.map(deployedPrimitive));
|
|
606
609
|
}
|
|
607
610
|
if (bindings.flowRegistry !== undefined && defaultProjectId !== undefined) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
+
deployedFlows.push(
|
|
612
|
+
...(await registerFlows({
|
|
613
|
+
flows: bindings.flowRegistry,
|
|
614
|
+
tools: bindings.toolRegistry,
|
|
615
|
+
agents: bindings.agentRegistry,
|
|
611
616
|
tenantId,
|
|
612
|
-
projectId:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
projectId: projectIdForGraph,
|
|
620
|
-
}),
|
|
621
|
-
});
|
|
622
|
-
if (outcome.kind === 'ok') {
|
|
623
|
-
const flowId = outcome.flowId;
|
|
624
|
-
const version = outcome.version;
|
|
625
|
-
rolled.push(async () => {
|
|
626
|
-
await bindings.flowRegistry?.unregister({ tenantId, flowId, version });
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
617
|
+
projectId: defaultProjectId,
|
|
618
|
+
defined: validated.flows,
|
|
619
|
+
rolled,
|
|
620
|
+
})),
|
|
621
|
+
);
|
|
622
|
+
} else {
|
|
623
|
+
deployedFlows.push(...validated.flows.map(deployedPrimitive));
|
|
630
624
|
}
|
|
631
625
|
} catch (cause) {
|
|
632
626
|
await rollback(rolled);
|
|
627
|
+
if (cause instanceof UnpinnableDeploy) {
|
|
628
|
+
c.status(statusFor('invalid-agent') as never);
|
|
629
|
+
return c.json(
|
|
630
|
+
toWireError(
|
|
631
|
+
{
|
|
632
|
+
code: 'validation-failed',
|
|
633
|
+
message: `The deployment's agents or flows use tool or agent versions that aren't published (${cause.issues.length} issue${cause.issues.length === 1 ? '' : 's'}); nothing was deployed`,
|
|
634
|
+
issues: cause.issues as unknown as Record<string, unknown>[],
|
|
635
|
+
},
|
|
636
|
+
requestId,
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
633
640
|
c.status(statusFor('internal-server-error') as never);
|
|
634
641
|
return c.json(
|
|
635
642
|
toWireError(
|
|
@@ -669,14 +676,8 @@ export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<App
|
|
|
669
676
|
version: t.version,
|
|
670
677
|
})),
|
|
671
678
|
guardrails: validated.guardrails.map((g) => ({ id: g.id as unknown as string })),
|
|
672
|
-
agents:
|
|
673
|
-
|
|
674
|
-
version: a.version as unknown as string,
|
|
675
|
-
})),
|
|
676
|
-
flows: validated.flows.map((f) => ({
|
|
677
|
-
id: f.id as unknown as string,
|
|
678
|
-
version: f.version as unknown as string,
|
|
679
|
-
})),
|
|
679
|
+
agents: deployedAgents,
|
|
680
|
+
flows: deployedFlows,
|
|
680
681
|
},
|
|
681
682
|
});
|
|
682
683
|
} catch (cause) {
|
|
@@ -1374,8 +1375,18 @@ function validateAgentIndexShape(
|
|
|
1374
1375
|
if (typeof spec.name !== 'string' || (spec.name as string).length === 0) {
|
|
1375
1376
|
out.push({ path: '/name', message: 'agent.name must be a non-empty string' });
|
|
1376
1377
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1378
|
+
const instructions = spec.instructions as { prompt?: unknown; version?: unknown } | string;
|
|
1379
|
+
const promptRef =
|
|
1380
|
+
typeof instructions === 'object' &&
|
|
1381
|
+
instructions !== null &&
|
|
1382
|
+
typeof instructions.prompt === 'string' &&
|
|
1383
|
+
typeof instructions.version === 'string';
|
|
1384
|
+
if (!promptRef && (typeof instructions !== 'string' || instructions.length === 0)) {
|
|
1385
|
+
out.push({
|
|
1386
|
+
path: '/instructions',
|
|
1387
|
+
message:
|
|
1388
|
+
'agent.instructions must be a non-empty string, or a prompt block { prompt, version }',
|
|
1389
|
+
});
|
|
1379
1390
|
}
|
|
1380
1391
|
if (!Array.isArray(spec.capabilities)) {
|
|
1381
1392
|
out.push({ path: '/capabilities', message: 'agent.capabilities must be an array' });
|
|
@@ -1411,6 +1422,205 @@ function validateFlowIndexShape(
|
|
|
1411
1422
|
|
|
1412
1423
|
type RollbackAction = () => Promise<void>;
|
|
1413
1424
|
|
|
1425
|
+
/** A deploy whose agents or flows use a tool or agent with no published version in range. */
|
|
1426
|
+
class UnpinnableDeploy extends Error {
|
|
1427
|
+
constructor(readonly issues: readonly UnpinnableRef[]) {
|
|
1428
|
+
super('the deployment uses tools or agents that are not published');
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** A deployed agent or flow registered under its definition's version. */
|
|
1433
|
+
function deployedPrimitive(definition: { readonly id: string; readonly version: string }): {
|
|
1434
|
+
id: string;
|
|
1435
|
+
version: string;
|
|
1436
|
+
} {
|
|
1437
|
+
return { id: definition.id, version: definition.version };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/** A deployed agent or flow, under the version the deploy rule registered it as. */
|
|
1441
|
+
function deployedVersion(
|
|
1442
|
+
definition: { readonly id: string; readonly version: string },
|
|
1443
|
+
outcome: DeployedVersionOutcome,
|
|
1444
|
+
): DeployedVersion {
|
|
1445
|
+
return outcome.kind === 'reused' || outcome.kind === 'renumbered'
|
|
1446
|
+
? {
|
|
1447
|
+
id: definition.id,
|
|
1448
|
+
version: outcome.version,
|
|
1449
|
+
authoredVersion: definition.version,
|
|
1450
|
+
reason: outcome.reason,
|
|
1451
|
+
newVersion: outcome.kind === 'renumbered',
|
|
1452
|
+
...(outcome.pinChanges !== undefined && { pinChanges: outcome.pinChanges }),
|
|
1453
|
+
}
|
|
1454
|
+
: deployedPrimitive(definition);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Register a deployment's agents, each under the version it's registered
|
|
1459
|
+
* as (`DeployedAgent`), pushing a rollback for each version it writes.
|
|
1460
|
+
*
|
|
1461
|
+
* With a tool registry, every agent is pinned first (the pack's tools
|
|
1462
|
+
* are registered by now): one whose range matches no published version
|
|
1463
|
+
* throws `UnpinnableDeploy` before any agent is written, and the deploy
|
|
1464
|
+
* rolls back. Each is then registered by `publishDeployedAgent`, which
|
|
1465
|
+
* never keeps a version's old pins. Without one, agents register
|
|
1466
|
+
* unpinned, as before pins existed.
|
|
1467
|
+
*/
|
|
1468
|
+
async function registerAgents(input: {
|
|
1469
|
+
readonly agents: AgentRegistryBinding;
|
|
1470
|
+
readonly tools: ToolRegistryBinding | undefined;
|
|
1471
|
+
readonly blocks: BlockRegistryBinding | undefined;
|
|
1472
|
+
readonly tenantId: TenantId;
|
|
1473
|
+
readonly projectId: ProjectId;
|
|
1474
|
+
readonly defined: readonly Agent[];
|
|
1475
|
+
readonly rolled: RollbackAction[];
|
|
1476
|
+
}): Promise<DeployedAgent[]> {
|
|
1477
|
+
const { agents, tools, blocks, tenantId, projectId, rolled } = input;
|
|
1478
|
+
// Signed deploys have no per-request principal — pass parent-only
|
|
1479
|
+
// tuples (no owner grant). Tenant admins keep access via
|
|
1480
|
+
// `admin from parent` cascade.
|
|
1481
|
+
const enqueueTuples = (agentId: string) =>
|
|
1482
|
+
tuplesForCreate({ kind: 'agent', id: agentId as AgentId, tenantId, projectId });
|
|
1483
|
+
const written = (agentId: AgentId, version: Semver) =>
|
|
1484
|
+
rolled.push(async () => {
|
|
1485
|
+
await agents.unregister({ tenantId, agentId, version });
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
if (tools === undefined) {
|
|
1489
|
+
for (const agent of input.defined) {
|
|
1490
|
+
const outcome = await agents.publish({ tenantId, projectId, agent, enqueueTuples });
|
|
1491
|
+
if (outcome.kind === 'ok') written(outcome.agentId, outcome.version);
|
|
1492
|
+
}
|
|
1493
|
+
return input.defined.map(deployedPrimitive);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const pinsOf = await pinAgents(tools, blocks, tenantId, input.defined);
|
|
1497
|
+
const deployed: DeployedAgent[] = [];
|
|
1498
|
+
for (const [i, agent] of input.defined.entries()) {
|
|
1499
|
+
const pins = pinsOf[i] as AgentPins;
|
|
1500
|
+
const outcome = await publishDeployedAgent({
|
|
1501
|
+
agents,
|
|
1502
|
+
tenantId,
|
|
1503
|
+
projectId,
|
|
1504
|
+
agent,
|
|
1505
|
+
pins,
|
|
1506
|
+
enqueueTuples,
|
|
1507
|
+
});
|
|
1508
|
+
if (outcome.kind === 'registered' || outcome.kind === 'renumbered') {
|
|
1509
|
+
written(agent.id, outcome.version as unknown as Semver);
|
|
1510
|
+
}
|
|
1511
|
+
deployed.push(deployedVersion(agent, outcome));
|
|
1512
|
+
}
|
|
1513
|
+
return deployed;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Register a deployment's flows, each under the version it's registered
|
|
1518
|
+
* as, pushing a rollback for each version it writes. Runs after the
|
|
1519
|
+
* agents, so a flow's agent pins see the versions this deploy
|
|
1520
|
+
* registered: a tool change cascades through an agent into a flow
|
|
1521
|
+
* within the one deploy, each derived once.
|
|
1522
|
+
*
|
|
1523
|
+
* With the tool and agent registries, every flow is pinned first; one
|
|
1524
|
+
* that runs a tool or agent with no published version throws
|
|
1525
|
+
* `UnpinnableDeploy` before any flow is written, and the deploy rolls
|
|
1526
|
+
* back. Without them, flows register unpinned, as before pins existed.
|
|
1527
|
+
*/
|
|
1528
|
+
async function registerFlows(input: {
|
|
1529
|
+
readonly flows: FlowRegistryBinding;
|
|
1530
|
+
readonly tools: ToolRegistryBinding | undefined;
|
|
1531
|
+
readonly agents: AgentRegistryBinding | undefined;
|
|
1532
|
+
readonly tenantId: TenantId;
|
|
1533
|
+
readonly projectId: ProjectId;
|
|
1534
|
+
readonly defined: readonly Flow[];
|
|
1535
|
+
readonly rolled: RollbackAction[];
|
|
1536
|
+
}): Promise<DeployedFlow[]> {
|
|
1537
|
+
const { flows, tools, agents, tenantId, projectId, rolled } = input;
|
|
1538
|
+
const enqueueTuples = (flowId: string) =>
|
|
1539
|
+
tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId });
|
|
1540
|
+
const written = (flowId: FlowId, version: string) =>
|
|
1541
|
+
rolled.push(async () => {
|
|
1542
|
+
await flows.unregister({ tenantId, flowId, version: version as never });
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
if (tools === undefined || agents === undefined) {
|
|
1546
|
+
for (const flow of input.defined) {
|
|
1547
|
+
const outcome = await flows.publish({ tenantId, projectId, flow, enqueueTuples });
|
|
1548
|
+
if (outcome.kind === 'ok') written(outcome.flowId, outcome.version as unknown as string);
|
|
1549
|
+
}
|
|
1550
|
+
return input.defined.map(deployedPrimitive);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const pinsOf = await pinFlows(tools, agents, tenantId, input.defined);
|
|
1554
|
+
const deployed: DeployedFlow[] = [];
|
|
1555
|
+
for (const [i, flow] of input.defined.entries()) {
|
|
1556
|
+
const outcome = await publishDeployedFlow({
|
|
1557
|
+
flows,
|
|
1558
|
+
tenantId,
|
|
1559
|
+
projectId,
|
|
1560
|
+
flow,
|
|
1561
|
+
pins: pinsOf[i] as FlowPins,
|
|
1562
|
+
enqueueTuples,
|
|
1563
|
+
});
|
|
1564
|
+
if (outcome.kind === 'registered' || outcome.kind === 'renumbered') {
|
|
1565
|
+
written(flow.id, outcome.version);
|
|
1566
|
+
}
|
|
1567
|
+
deployed.push(deployedVersion(flow, outcome));
|
|
1568
|
+
}
|
|
1569
|
+
return deployed;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/** Each flow's pins, in order; throws `UnpinnableDeploy` naming every tool or agent with no published version. */
|
|
1573
|
+
async function pinFlows(
|
|
1574
|
+
tools: ToolRegistryBinding,
|
|
1575
|
+
agents: AgentRegistryBinding,
|
|
1576
|
+
tenantId: TenantId,
|
|
1577
|
+
defined: readonly Flow[],
|
|
1578
|
+
): Promise<FlowPins[]> {
|
|
1579
|
+
const pins: FlowPins[] = [];
|
|
1580
|
+
const unpinnable: UnpinnableRef[] = [];
|
|
1581
|
+
for (const [i, flow] of defined.entries()) {
|
|
1582
|
+
const resolved = await resolveFlowPins(tools, agents, tenantId, flow);
|
|
1583
|
+
if (resolved.kind === 'ok') {
|
|
1584
|
+
pins.push(resolved.pins);
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
for (const issue of resolved.issues) {
|
|
1588
|
+
unpinnable.push({
|
|
1589
|
+
path: `/flows/${i}${issue.path}`,
|
|
1590
|
+
message: `flow "${flow.id as unknown as string}": ${issue.message}`,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (unpinnable.length > 0) throw new UnpinnableDeploy(unpinnable);
|
|
1595
|
+
return pins;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Each agent's pins, in order; throws `UnpinnableDeploy` naming every range that matches nothing. */
|
|
1599
|
+
async function pinAgents(
|
|
1600
|
+
tools: ToolRegistryBinding,
|
|
1601
|
+
blocks: BlockRegistryBinding | undefined,
|
|
1602
|
+
tenantId: TenantId,
|
|
1603
|
+
defined: readonly Agent[],
|
|
1604
|
+
): Promise<AgentPins[]> {
|
|
1605
|
+
const pins: AgentPins[] = [];
|
|
1606
|
+
const unpinnable: UnpinnableRef[] = [];
|
|
1607
|
+
for (const [i, agent] of defined.entries()) {
|
|
1608
|
+
const resolved = await resolveAgentPins(tools, tenantId, agent, blocks);
|
|
1609
|
+
if (resolved.kind === 'ok') {
|
|
1610
|
+
pins.push(resolved.pins);
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
for (const issue of resolved.issues) {
|
|
1614
|
+
unpinnable.push({
|
|
1615
|
+
path: `/agents/${i}${issue.path}`,
|
|
1616
|
+
message: `agent "${agent.id as unknown as string}": ${issue.message}`,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (unpinnable.length > 0) throw new UnpinnableDeploy(unpinnable);
|
|
1621
|
+
return pins;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1414
1624
|
async function rollback(actions: readonly RollbackAction[]): Promise<void> {
|
|
1415
1625
|
// Reverse order — last-in, first-out — so registrations are undone in
|
|
1416
1626
|
// the mirror sequence of their creation.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { AgentId } from '@kindgi/agents';
|
|
5
|
+
import type { ProjectId, Semver } from '@kindgi/types';
|
|
6
|
+
|
|
7
|
+
import type { EvalBaseline, EvalComparison } from '../eval-run-binding.js';
|
|
8
|
+
import { DEFAULT_COMPARISON } from '../judged-dispatcher.js';
|
|
9
|
+
|
|
10
|
+
const MAX_REPETITIONS = 10;
|
|
11
|
+
const MAX_K = 100;
|
|
12
|
+
|
|
13
|
+
function obj(value: unknown): Record<string, unknown> | undefined {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
15
|
+
? (value as Record<string, unknown>)
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseLive(raw: unknown): EvalBaseline | string {
|
|
20
|
+
const live = obj(raw);
|
|
21
|
+
if (live === undefined) return '`baseline.live` must be an object';
|
|
22
|
+
const segments = live.segments === undefined ? undefined : obj(live.segments);
|
|
23
|
+
const stringsOnly =
|
|
24
|
+
segments !== undefined && Object.values(segments).every((v) => typeof v === 'string');
|
|
25
|
+
if (live.segments !== undefined && !stringsOnly) {
|
|
26
|
+
return '`baseline.live.segments` must be an object of strings';
|
|
27
|
+
}
|
|
28
|
+
if (live.projectId !== undefined && typeof live.projectId !== 'string') {
|
|
29
|
+
return '`baseline.live.projectId` must be a string';
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
live: {
|
|
33
|
+
...(typeof live.projectId === 'string' && { projectId: live.projectId as ProjectId }),
|
|
34
|
+
...(segments !== undefined && { segments: segments as Record<string, string> }),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseBaseline(raw: unknown): EvalBaseline | string {
|
|
40
|
+
if (raw === 'recorded') return 'recorded';
|
|
41
|
+
const o = obj(raw);
|
|
42
|
+
if (o === undefined) {
|
|
43
|
+
return "`baseline` must be 'recorded', { agentId, version } or { live: { projectId?, segments? } }";
|
|
44
|
+
}
|
|
45
|
+
if (o.live !== undefined) return parseLive(o.live);
|
|
46
|
+
if (typeof o.agentId !== 'string' || o.agentId === '' || typeof o.version !== 'string') {
|
|
47
|
+
return '`baseline` as a version needs `agentId` and `version`';
|
|
48
|
+
}
|
|
49
|
+
return { agentId: o.agentId as AgentId, version: o.version as Semver };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function integerIn(raw: unknown, name: string, max: number): number | string {
|
|
53
|
+
return typeof raw === 'number' && Number.isInteger(raw) && raw >= 1 && raw <= max
|
|
54
|
+
? raw
|
|
55
|
+
: `\`${name}\` must be an integer from 1 to ${max}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A comparison eval run's settings from the start body: `baseline`,
|
|
60
|
+
* `reads`, `repetitions` and `k`. `undefined` when none is given (the
|
|
61
|
+
* defaults apply); an error message for a bad one.
|
|
62
|
+
*/
|
|
63
|
+
export function parseComparison(
|
|
64
|
+
b: Readonly<Record<string, unknown>>,
|
|
65
|
+
):
|
|
66
|
+
| { readonly kind: 'ok'; readonly value?: EvalComparison }
|
|
67
|
+
| { readonly kind: 'err'; readonly message: string } {
|
|
68
|
+
const { baseline, reads, repetitions, k } = b;
|
|
69
|
+
if (
|
|
70
|
+
baseline === undefined &&
|
|
71
|
+
reads === undefined &&
|
|
72
|
+
repetitions === undefined &&
|
|
73
|
+
k === undefined
|
|
74
|
+
) {
|
|
75
|
+
return { kind: 'ok' };
|
|
76
|
+
}
|
|
77
|
+
const parsedBaseline =
|
|
78
|
+
baseline === undefined ? DEFAULT_COMPARISON.baseline : parseBaseline(baseline);
|
|
79
|
+
if (typeof parsedBaseline === 'string' && parsedBaseline !== 'recorded') {
|
|
80
|
+
return { kind: 'err', message: parsedBaseline };
|
|
81
|
+
}
|
|
82
|
+
if (reads !== undefined && reads !== 'recorded' && reads !== 'live') {
|
|
83
|
+
return { kind: 'err', message: "`reads` must be 'recorded' or 'live'" };
|
|
84
|
+
}
|
|
85
|
+
const reps =
|
|
86
|
+
repetitions === undefined
|
|
87
|
+
? DEFAULT_COMPARISON.repetitions
|
|
88
|
+
: integerIn(repetitions, 'repetitions', MAX_REPETITIONS);
|
|
89
|
+
if (typeof reps === 'string') return { kind: 'err', message: reps };
|
|
90
|
+
const topK = k === undefined ? DEFAULT_COMPARISON.k : integerIn(k, 'k', MAX_K);
|
|
91
|
+
if (typeof topK === 'string') return { kind: 'err', message: topK };
|
|
92
|
+
return {
|
|
93
|
+
kind: 'ok',
|
|
94
|
+
value: {
|
|
95
|
+
baseline: parsedBaseline,
|
|
96
|
+
reads: reads ?? DEFAULT_COMPARISON.reads,
|
|
97
|
+
repetitions: reps,
|
|
98
|
+
k: topK,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
package/src/routes/eval-runs.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { statusFor, toWireError } from '../errors.js';
|
|
|
10
10
|
import {
|
|
11
11
|
type AgentRef,
|
|
12
12
|
EVAL_RUN_STATUSES,
|
|
13
|
+
type EvalComparison,
|
|
13
14
|
type EvalRun,
|
|
14
15
|
type EvalRunBinding,
|
|
15
16
|
type EvalRunFilter,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type FlowRef,
|
|
18
19
|
} from '../eval-run-binding.js';
|
|
19
20
|
import type { AppEnv } from '../types.js';
|
|
21
|
+
import { parseComparison } from './eval-comparison.js';
|
|
20
22
|
import { clampLimit } from './pagination.js';
|
|
21
23
|
import { parseScopeParams } from './scope-params.js';
|
|
22
24
|
import { formatSseFrame } from './sse.js';
|
|
@@ -93,6 +95,7 @@ function startRouter(binding: EvalRunBinding): Hono<AppEnv> {
|
|
|
93
95
|
...(parsed.value.correlationId !== undefined && {
|
|
94
96
|
correlationId: parsed.value.correlationId,
|
|
95
97
|
}),
|
|
98
|
+
...(parsed.value.comparison !== undefined && { comparison: parsed.value.comparison }),
|
|
96
99
|
});
|
|
97
100
|
if (outcome.kind === 'suite-not-found') {
|
|
98
101
|
c.status(statusFor('eval-suite-not-found') as never);
|
|
@@ -454,6 +457,7 @@ function serializeEvalRun(run: EvalRun): Record<string, unknown> {
|
|
|
454
457
|
...(run.result !== undefined && { result: run.result }),
|
|
455
458
|
...(run.error !== undefined && { error: run.error }),
|
|
456
459
|
...(run.correlationId !== undefined && { correlationId: run.correlationId }),
|
|
460
|
+
...(run.comparison !== undefined && { comparison: run.comparison }),
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
463
|
|
|
@@ -486,6 +490,7 @@ interface ParsedStartBody {
|
|
|
486
490
|
readonly flowRef?: FlowRef;
|
|
487
491
|
readonly dryRun?: boolean;
|
|
488
492
|
readonly correlationId?: string;
|
|
493
|
+
readonly comparison?: EvalComparison;
|
|
489
494
|
}
|
|
490
495
|
|
|
491
496
|
function parseStartBody(
|
|
@@ -618,6 +623,11 @@ function parseStartBody(
|
|
|
618
623
|
};
|
|
619
624
|
}
|
|
620
625
|
|
|
626
|
+
const comparison = parseComparison(b);
|
|
627
|
+
if (comparison.kind === 'err') {
|
|
628
|
+
return { kind: 'err', error: { code: 'bad-input', message: comparison.message } };
|
|
629
|
+
}
|
|
630
|
+
|
|
621
631
|
return {
|
|
622
632
|
kind: 'ok',
|
|
623
633
|
value: {
|
|
@@ -626,6 +636,7 @@ function parseStartBody(
|
|
|
626
636
|
...(flowRef !== undefined && { flowRef }),
|
|
627
637
|
...(dryRun !== undefined && { dryRun }),
|
|
628
638
|
...(correlationId !== undefined && { correlationId: correlationId as string }),
|
|
639
|
+
...(comparison.value !== undefined && { comparison: comparison.value }),
|
|
629
640
|
},
|
|
630
641
|
};
|
|
631
642
|
}
|
package/src/routes/flows.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
|
|
6
6
|
import { type Principal, ref, tuplesForCreate } from '@kindgi/authz';
|
|
7
|
-
import { type Flow, type FlowError, loadFlow } from '@kindgi/flow';
|
|
7
|
+
import { type Flow, type FlowError, flowPinsDigest, loadFlow } from '@kindgi/flow';
|
|
8
8
|
import type { Cursor, FlowId, ProjectId, TenantId, UserId } from '@kindgi/types';
|
|
9
9
|
|
|
10
|
+
import type { AgentRegistryBinding } from '../agent-binding.js';
|
|
11
|
+
import type { UnpinnableRef } from '../agent-pins.js';
|
|
10
12
|
import { statusFor, toWireError } from '../errors.js';
|
|
11
|
-
import type { FlowRegistryBinding } from '../flow-binding.js';
|
|
13
|
+
import type { FlowRegistryBinding, FlowVersionRecord } from '../flow-binding.js';
|
|
14
|
+
import { resolveFlowPins } from '../flow-pins.js';
|
|
12
15
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
16
|
+
import type { ToolRegistryBinding } from '../tool-binding.js';
|
|
13
17
|
import type { AppEnv } from '../types.js';
|
|
14
18
|
import { clampLimit } from './pagination.js';
|
|
15
19
|
import { parseScopeParams } from './scope-params.js';
|
|
@@ -30,8 +34,19 @@ import { parseScopeParams } from './scope-params.js';
|
|
|
30
34
|
* Run initiation stays at `POST /v1/runs` (one primitive,
|
|
31
35
|
* discriminated subject). No
|
|
32
36
|
* `/v1/flows/:flowId/run` route is added here.
|
|
37
|
+
*
|
|
38
|
+
* With `pinning` (the tool and agent registries), a published version is
|
|
39
|
+
* pinned: each tool it runs, and each agent it runs at no named version,
|
|
40
|
+
* resolves once, at publish, to the latest version, which every run of
|
|
41
|
+
* that version uses (`pins`, see `resolveFlowPins`); one with no
|
|
42
|
+
* published version refuses the publish. Without it, versions carry no
|
|
43
|
+
* pins and bind the latest versions per run.
|
|
33
44
|
*/
|
|
34
|
-
export function flowsRouter(
|
|
45
|
+
export function flowsRouter(
|
|
46
|
+
binding: FlowRegistryBinding,
|
|
47
|
+
authorizer?: Authorizer,
|
|
48
|
+
pinning?: { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding },
|
|
49
|
+
): Hono<AppEnv> {
|
|
35
50
|
const r = new Hono<AppEnv>();
|
|
36
51
|
|
|
37
52
|
// Authorization — mirrors agents 1:1. Check the resource directly;
|
|
@@ -275,13 +290,23 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
|
|
|
275
290
|
);
|
|
276
291
|
}
|
|
277
292
|
|
|
293
|
+
// Pin the version: every tool and unversioned agent resolves now,
|
|
294
|
+
// once, to the version its runs use. One with no published version
|
|
295
|
+
// refuses the publish rather than store a partly pinned version.
|
|
296
|
+
const pinned = await pinFlow(pinning, tenantId, loaded.value);
|
|
297
|
+
if (pinned.kind === 'unpinnable') {
|
|
298
|
+
c.status(statusFor('validation-failed') as never);
|
|
299
|
+
return c.json(toWireError(unpinnableFlow(loaded.value, pinned.issues), requestId));
|
|
300
|
+
}
|
|
301
|
+
const flow = pinned.flow;
|
|
302
|
+
|
|
278
303
|
const principal = c.get('principal') as Principal | undefined;
|
|
279
304
|
const creatorUserId =
|
|
280
305
|
principal?.actor.kind === 'user' ? (principal.actor.id as UserId) : undefined;
|
|
281
306
|
const outcome = await binding.publish({
|
|
282
307
|
tenantId,
|
|
283
308
|
projectId,
|
|
284
|
-
flow
|
|
309
|
+
flow,
|
|
285
310
|
enqueueTuples: (flowId) =>
|
|
286
311
|
tuplesForCreate({ kind: 'flow', id: flowId as FlowId, tenantId, projectId }, creatorUserId),
|
|
287
312
|
});
|
|
@@ -383,7 +408,36 @@ export function flowsRouter(binding: FlowRegistryBinding, authorizer?: Authorize
|
|
|
383
408
|
return r;
|
|
384
409
|
}
|
|
385
410
|
|
|
386
|
-
|
|
411
|
+
/** The flow with its pins, or the references with no published version; as it is without `pinning`. */
|
|
412
|
+
async function pinFlow(
|
|
413
|
+
pinning:
|
|
414
|
+
| { readonly tools: ToolRegistryBinding; readonly agents: AgentRegistryBinding }
|
|
415
|
+
| undefined,
|
|
416
|
+
tenantId: TenantId,
|
|
417
|
+
flow: Flow,
|
|
418
|
+
): Promise<
|
|
419
|
+
| { readonly kind: 'ok'; readonly flow: Flow }
|
|
420
|
+
| { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] }
|
|
421
|
+
> {
|
|
422
|
+
if (pinning === undefined) return { kind: 'ok', flow };
|
|
423
|
+
const resolved = await resolveFlowPins(pinning.tools, pinning.agents, tenantId, flow);
|
|
424
|
+
if (resolved.kind === 'unpinnable') return resolved;
|
|
425
|
+
return {
|
|
426
|
+
kind: 'ok',
|
|
427
|
+
flow: { ...flow, pins: resolved.pins, pinsDigest: flowPinsDigest(resolved.pins) },
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** The `validation-failed` error naming each tool or agent a flow runs that isn't published. */
|
|
432
|
+
function unpinnableFlow(flow: Flow, issues: readonly UnpinnableRef[]) {
|
|
433
|
+
return {
|
|
434
|
+
code: 'validation-failed' as const,
|
|
435
|
+
message: `Flow "${flow.id as unknown as string}" runs tools or agents that aren't published (${issues.length} issue${issues.length === 1 ? '' : 's'})`,
|
|
436
|
+
issues: issues as unknown as Record<string, unknown>[],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function serializeGraph(g: FlowVersionRecord): Record<string, unknown> {
|
|
387
441
|
return {
|
|
388
442
|
id: g.id as unknown as string,
|
|
389
443
|
version: g.version,
|
|
@@ -393,6 +447,10 @@ function serializeGraph(g: Flow): Record<string, unknown> {
|
|
|
393
447
|
edges: g.edges,
|
|
394
448
|
...(g.maxParallelism !== undefined && { maxParallelism: g.maxParallelism }),
|
|
395
449
|
...(g.metadata !== undefined && { metadata: g.metadata }),
|
|
450
|
+
...(g.pins !== undefined && { pins: g.pins }),
|
|
451
|
+
...(g.pinsDigest !== undefined && { pinsDigest: g.pinsDigest }),
|
|
452
|
+
...(g.derivedFrom !== undefined && { derivedFrom: g.derivedFrom }),
|
|
453
|
+
...(g.unregisteredAt !== undefined && { unregisteredAt: g.unregisteredAt }),
|
|
396
454
|
};
|
|
397
455
|
}
|
|
398
456
|
|