@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.
Files changed (193) hide show
  1. package/dist/agent-binding.d.ts +18 -4
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts +48 -0
  4. package/dist/agent-pins.d.ts.map +1 -0
  5. package/dist/agent-pins.js +102 -0
  6. package/dist/agent-pins.js.map +1 -0
  7. package/dist/app.d.ts +22 -0
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +20 -2
  10. package/dist/app.js.map +1 -1
  11. package/dist/block-binding.d.ts +132 -0
  12. package/dist/block-binding.d.ts.map +1 -0
  13. package/dist/block-binding.js +4 -0
  14. package/dist/block-binding.js.map +1 -0
  15. package/dist/block-pins.d.ts +22 -0
  16. package/dist/block-pins.d.ts.map +1 -0
  17. package/dist/block-pins.js +112 -0
  18. package/dist/block-pins.js.map +1 -0
  19. package/dist/cost-binding.d.ts +20 -1
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +4 -0
  22. package/dist/cost-binding.js.map +1 -1
  23. package/dist/deploy-versions.d.ts +58 -0
  24. package/dist/deploy-versions.d.ts.map +1 -0
  25. package/dist/deploy-versions.js +91 -0
  26. package/dist/deploy-versions.js.map +1 -0
  27. package/dist/deployment-binding.d.ts +24 -3
  28. package/dist/deployment-binding.d.ts.map +1 -1
  29. package/dist/derive-agent-version.d.ts +69 -0
  30. package/dist/derive-agent-version.d.ts.map +1 -0
  31. package/dist/derive-agent-version.js +139 -0
  32. package/dist/derive-agent-version.js.map +1 -0
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +17 -0
  35. package/dist/errors.js.map +1 -1
  36. package/dist/eval-case-binding.d.ts +65 -0
  37. package/dist/eval-case-binding.d.ts.map +1 -0
  38. package/dist/eval-case-binding.js +4 -0
  39. package/dist/eval-case-binding.js.map +1 -0
  40. package/dist/eval-run-binding.d.ts +30 -0
  41. package/dist/eval-run-binding.d.ts.map +1 -1
  42. package/dist/eval-run-dispatcher.d.ts +38 -3
  43. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  44. package/dist/eval-run-dispatcher.js +21 -15
  45. package/dist/eval-run-dispatcher.js.map +1 -1
  46. package/dist/eval-suite-binding.d.ts +1 -1
  47. package/dist/eval-suite-binding.d.ts.map +1 -1
  48. package/dist/eval-suite-binding.js +2 -0
  49. package/dist/eval-suite-binding.js.map +1 -1
  50. package/dist/flow-binding.d.ts +10 -4
  51. package/dist/flow-binding.d.ts.map +1 -1
  52. package/dist/flow-pins.d.ts +36 -0
  53. package/dist/flow-pins.d.ts.map +1 -0
  54. package/dist/flow-pins.js +81 -0
  55. package/dist/flow-pins.js.map +1 -0
  56. package/dist/index.d.ts +17 -6
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +8 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/judged-dispatcher.d.ts +134 -0
  61. package/dist/judged-dispatcher.d.ts.map +1 -0
  62. package/dist/judged-dispatcher.js +297 -0
  63. package/dist/judged-dispatcher.js.map +1 -0
  64. package/dist/judged-items.d.ts +86 -0
  65. package/dist/judged-items.d.ts.map +1 -0
  66. package/dist/judged-items.js +184 -0
  67. package/dist/judged-items.js.map +1 -0
  68. package/dist/judgment-binding.d.ts +316 -0
  69. package/dist/judgment-binding.d.ts.map +1 -0
  70. package/dist/judgment-binding.js +19 -0
  71. package/dist/judgment-binding.js.map +1 -0
  72. package/dist/openapi/generate.d.ts.map +1 -1
  73. package/dist/openapi/generate.js +4 -1
  74. package/dist/openapi/generate.js.map +1 -1
  75. package/dist/openapi/operations.d.ts.map +1 -1
  76. package/dist/openapi/operations.js +461 -7
  77. package/dist/openapi/operations.js.map +1 -1
  78. package/dist/openapi/schemas.d.ts +40 -0
  79. package/dist/openapi/schemas.d.ts.map +1 -1
  80. package/dist/openapi/schemas.js +843 -7
  81. package/dist/openapi/schemas.js.map +1 -1
  82. package/dist/provider-binding.d.ts +12 -7
  83. package/dist/provider-binding.d.ts.map +1 -1
  84. package/dist/routes/agents.d.ts +9 -1
  85. package/dist/routes/agents.d.ts.map +1 -1
  86. package/dist/routes/agents.js +175 -11
  87. package/dist/routes/agents.js.map +1 -1
  88. package/dist/routes/blocks.d.ts +19 -0
  89. package/dist/routes/blocks.d.ts.map +1 -0
  90. package/dist/routes/blocks.js +281 -0
  91. package/dist/routes/blocks.js.map +1 -0
  92. package/dist/routes/cost.d.ts.map +1 -1
  93. package/dist/routes/cost.js +47 -2
  94. package/dist/routes/cost.js.map +1 -1
  95. package/dist/routes/deployments.d.ts +3 -0
  96. package/dist/routes/deployments.d.ts.map +1 -1
  97. package/dist/routes/deployments.js +208 -55
  98. package/dist/routes/deployments.js.map +1 -1
  99. package/dist/routes/eval-comparison.d.ts +14 -0
  100. package/dist/routes/eval-comparison.d.ts.map +1 -0
  101. package/dist/routes/eval-comparison.js +87 -0
  102. package/dist/routes/eval-comparison.js.map +1 -0
  103. package/dist/routes/eval-runs.d.ts.map +1 -1
  104. package/dist/routes/eval-runs.js +8 -0
  105. package/dist/routes/eval-runs.js.map +1 -1
  106. package/dist/routes/flows.d.ts +13 -1
  107. package/dist/routes/flows.d.ts.map +1 -1
  108. package/dist/routes/flows.js +44 -3
  109. package/dist/routes/flows.js.map +1 -1
  110. package/dist/routes/hierarchy-errors.d.ts +35 -0
  111. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  112. package/dist/routes/hierarchy-errors.js +39 -0
  113. package/dist/routes/hierarchy-errors.js.map +1 -0
  114. package/dist/routes/judged-suites.d.ts +20 -0
  115. package/dist/routes/judged-suites.d.ts.map +1 -0
  116. package/dist/routes/judged-suites.js +272 -0
  117. package/dist/routes/judged-suites.js.map +1 -0
  118. package/dist/routes/judgment-context.d.ts +22 -0
  119. package/dist/routes/judgment-context.d.ts.map +1 -0
  120. package/dist/routes/judgment-context.js +88 -0
  121. package/dist/routes/judgment-context.js.map +1 -0
  122. package/dist/routes/judgment-flow-context.d.ts +32 -0
  123. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  124. package/dist/routes/judgment-flow-context.js +195 -0
  125. package/dist/routes/judgment-flow-context.js.map +1 -0
  126. package/dist/routes/judgments.d.ts +41 -0
  127. package/dist/routes/judgments.d.ts.map +1 -0
  128. package/dist/routes/judgments.js +566 -0
  129. package/dist/routes/judgments.js.map +1 -0
  130. package/dist/routes/orgs.d.ts +5 -2
  131. package/dist/routes/orgs.d.ts.map +1 -1
  132. package/dist/routes/orgs.js +38 -22
  133. package/dist/routes/orgs.js.map +1 -1
  134. package/dist/routes/policies.d.ts.map +1 -1
  135. package/dist/routes/policies.js +12 -1
  136. package/dist/routes/policies.js.map +1 -1
  137. package/dist/routes/projects.d.ts +10 -2
  138. package/dist/routes/projects.d.ts.map +1 -1
  139. package/dist/routes/projects.js +87 -79
  140. package/dist/routes/projects.js.map +1 -1
  141. package/dist/routes/providers.d.ts.map +1 -1
  142. package/dist/routes/providers.js +6 -1
  143. package/dist/routes/providers.js.map +1 -1
  144. package/dist/routes/runs.js +24 -3
  145. package/dist/routes/runs.js.map +1 -1
  146. package/dist/routes/teams.d.ts +6 -2
  147. package/dist/routes/teams.d.ts.map +1 -1
  148. package/dist/routes/teams.js +77 -73
  149. package/dist/routes/teams.js.map +1 -1
  150. package/openapi.json +13545 -10103
  151. package/package.json +21 -21
  152. package/src/agent-binding.ts +19 -4
  153. package/src/agent-pins.ts +147 -0
  154. package/src/app.ts +71 -2
  155. package/src/block-binding.ts +137 -0
  156. package/src/block-pins.ts +148 -0
  157. package/src/cost-binding.ts +21 -1
  158. package/src/deploy-versions.ts +157 -0
  159. package/src/deployment-binding.ts +27 -3
  160. package/src/derive-agent-version.ts +206 -0
  161. package/src/errors.ts +17 -0
  162. package/src/eval-case-binding.ts +71 -0
  163. package/src/eval-run-binding.ts +33 -0
  164. package/src/eval-run-dispatcher.ts +57 -16
  165. package/src/eval-suite-binding.ts +2 -0
  166. package/src/flow-binding.ts +11 -4
  167. package/src/flow-pins.ts +113 -0
  168. package/src/index.ts +85 -2
  169. package/src/judged-dispatcher.ts +507 -0
  170. package/src/judged-items.ts +263 -0
  171. package/src/judgment-binding.ts +349 -0
  172. package/src/openapi/generate.ts +7 -1
  173. package/src/openapi/operations.ts +523 -7
  174. package/src/openapi/schemas.ts +1009 -88
  175. package/src/provider-binding.ts +12 -7
  176. package/src/routes/agents.ts +243 -19
  177. package/src/routes/blocks.ts +362 -0
  178. package/src/routes/cost.ts +55 -1
  179. package/src/routes/deployments.ts +266 -56
  180. package/src/routes/eval-comparison.ts +101 -0
  181. package/src/routes/eval-runs.ts +11 -0
  182. package/src/routes/flows.ts +63 -5
  183. package/src/routes/hierarchy-errors.ts +51 -0
  184. package/src/routes/judged-suites.ts +363 -0
  185. package/src/routes/judgment-context.ts +128 -0
  186. package/src/routes/judgment-flow-context.ts +245 -0
  187. package/src/routes/judgments.ts +743 -0
  188. package/src/routes/orgs.ts +44 -27
  189. package/src/routes/policies.ts +19 -0
  190. package/src/routes/projects.ts +106 -95
  191. package/src/routes/providers.ts +5 -0
  192. package/src/routes/runs.ts +28 -3
  193. 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
- for (const agent of validated.agents) {
582
- const projectIdForAgent = defaultProjectId;
583
- const outcome = await bindings.agentRegistry.publish({
596
+ deployedAgents.push(
597
+ ...(await registerAgents({
598
+ agents: bindings.agentRegistry,
599
+ tools: bindings.toolRegistry,
600
+ blocks: bindings.blockRegistry,
584
601
  tenantId,
585
- projectId: projectIdForAgent,
586
- agent,
587
- // Signed deploys have no per-request principal — pass parent-
588
- // only tuples (no owner grant). Tenant admins keep access via
589
- // `admin from parent` cascade.
590
- enqueueTuples: (agentId) =>
591
- tuplesForCreate({
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
- for (const flow of validated.flows) {
609
- const projectIdForGraph = defaultProjectId;
610
- const outcome = await bindings.flowRegistry.publish({
611
+ deployedFlows.push(
612
+ ...(await registerFlows({
613
+ flows: bindings.flowRegistry,
614
+ tools: bindings.toolRegistry,
615
+ agents: bindings.agentRegistry,
611
616
  tenantId,
612
- projectId: projectIdForGraph,
613
- flow,
614
- enqueueTuples: (flowId) =>
615
- tuplesForCreate({
616
- kind: 'flow',
617
- id: flowId as FlowId,
618
- tenantId,
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: validated.agents.map((a) => ({
673
- id: a.id as unknown as string,
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
- if (typeof spec.instructions !== 'string' || (spec.instructions as string).length === 0) {
1378
- out.push({ path: '/instructions', message: 'agent.instructions must be a non-empty string' });
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
+ }
@@ -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
  }
@@ -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(binding: FlowRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
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: loaded.value,
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
- function serializeGraph(g: Flow): Record<string, unknown> {
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