@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.
Files changed (250) 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 +22 -3
  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 +100 -5
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +10 -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/hitl-binding.d.ts +20 -5
  57. package/dist/hitl-binding.d.ts.map +1 -1
  58. package/dist/index.d.ts +19 -8
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +8 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/judged-dispatcher.d.ts +134 -0
  63. package/dist/judged-dispatcher.d.ts.map +1 -0
  64. package/dist/judged-dispatcher.js +297 -0
  65. package/dist/judged-dispatcher.js.map +1 -0
  66. package/dist/judged-items.d.ts +86 -0
  67. package/dist/judged-items.d.ts.map +1 -0
  68. package/dist/judged-items.js +184 -0
  69. package/dist/judged-items.js.map +1 -0
  70. package/dist/judgment-binding.d.ts +316 -0
  71. package/dist/judgment-binding.d.ts.map +1 -0
  72. package/dist/judgment-binding.js +19 -0
  73. package/dist/judgment-binding.js.map +1 -0
  74. package/dist/middleware/auth.d.ts +3 -2
  75. package/dist/middleware/auth.d.ts.map +1 -1
  76. package/dist/middleware/auth.js.map +1 -1
  77. package/dist/middleware/idempotency.d.ts +5 -1
  78. package/dist/middleware/idempotency.d.ts.map +1 -1
  79. package/dist/middleware/idempotency.js +8 -1
  80. package/dist/middleware/idempotency.js.map +1 -1
  81. package/dist/openapi/generate.d.ts.map +1 -1
  82. package/dist/openapi/generate.js +4 -1
  83. package/dist/openapi/generate.js.map +1 -1
  84. package/dist/openapi/operations.d.ts.map +1 -1
  85. package/dist/openapi/operations.js +543 -24
  86. package/dist/openapi/operations.js.map +1 -1
  87. package/dist/openapi/schemas.d.ts +58 -0
  88. package/dist/openapi/schemas.d.ts.map +1 -1
  89. package/dist/openapi/schemas.js +1058 -27
  90. package/dist/openapi/schemas.js.map +1 -1
  91. package/dist/provenance-binding.d.ts +27 -1
  92. package/dist/provenance-binding.d.ts.map +1 -1
  93. package/dist/provenance-binding.js.map +1 -1
  94. package/dist/provider-binding.d.ts +12 -7
  95. package/dist/provider-binding.d.ts.map +1 -1
  96. package/dist/reviewer-binding.d.ts +10 -2
  97. package/dist/reviewer-binding.d.ts.map +1 -1
  98. package/dist/reviewer-role.d.ts +13 -0
  99. package/dist/reviewer-role.d.ts.map +1 -0
  100. package/dist/reviewer-role.js +27 -0
  101. package/dist/reviewer-role.js.map +1 -0
  102. package/dist/routes/agents.d.ts +9 -1
  103. package/dist/routes/agents.d.ts.map +1 -1
  104. package/dist/routes/agents.js +175 -11
  105. package/dist/routes/agents.js.map +1 -1
  106. package/dist/routes/approvals.d.ts +5 -4
  107. package/dist/routes/approvals.d.ts.map +1 -1
  108. package/dist/routes/approvals.js +52 -16
  109. package/dist/routes/approvals.js.map +1 -1
  110. package/dist/routes/blocks.d.ts +19 -0
  111. package/dist/routes/blocks.d.ts.map +1 -0
  112. package/dist/routes/blocks.js +281 -0
  113. package/dist/routes/blocks.js.map +1 -0
  114. package/dist/routes/conversations.d.ts +7 -1
  115. package/dist/routes/conversations.d.ts.map +1 -1
  116. package/dist/routes/conversations.js +41 -1
  117. package/dist/routes/conversations.js.map +1 -1
  118. package/dist/routes/cost.d.ts.map +1 -1
  119. package/dist/routes/cost.js +142 -11
  120. package/dist/routes/cost.js.map +1 -1
  121. package/dist/routes/deployments.d.ts +3 -0
  122. package/dist/routes/deployments.d.ts.map +1 -1
  123. package/dist/routes/deployments.js +208 -55
  124. package/dist/routes/deployments.js.map +1 -1
  125. package/dist/routes/eval-comparison.d.ts +14 -0
  126. package/dist/routes/eval-comparison.d.ts.map +1 -0
  127. package/dist/routes/eval-comparison.js +87 -0
  128. package/dist/routes/eval-comparison.js.map +1 -0
  129. package/dist/routes/eval-runs.d.ts.map +1 -1
  130. package/dist/routes/eval-runs.js +8 -0
  131. package/dist/routes/eval-runs.js.map +1 -1
  132. package/dist/routes/flows.d.ts +13 -1
  133. package/dist/routes/flows.d.ts.map +1 -1
  134. package/dist/routes/flows.js +44 -3
  135. package/dist/routes/flows.js.map +1 -1
  136. package/dist/routes/hierarchy-errors.d.ts +35 -0
  137. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  138. package/dist/routes/hierarchy-errors.js +39 -0
  139. package/dist/routes/hierarchy-errors.js.map +1 -0
  140. package/dist/routes/identity.d.ts +7 -0
  141. package/dist/routes/identity.d.ts.map +1 -1
  142. package/dist/routes/identity.js +3 -2
  143. package/dist/routes/identity.js.map +1 -1
  144. package/dist/routes/judged-suites.d.ts +20 -0
  145. package/dist/routes/judged-suites.d.ts.map +1 -0
  146. package/dist/routes/judged-suites.js +272 -0
  147. package/dist/routes/judged-suites.js.map +1 -0
  148. package/dist/routes/judgment-context.d.ts +22 -0
  149. package/dist/routes/judgment-context.d.ts.map +1 -0
  150. package/dist/routes/judgment-context.js +88 -0
  151. package/dist/routes/judgment-context.js.map +1 -0
  152. package/dist/routes/judgment-flow-context.d.ts +32 -0
  153. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  154. package/dist/routes/judgment-flow-context.js +195 -0
  155. package/dist/routes/judgment-flow-context.js.map +1 -0
  156. package/dist/routes/judgments.d.ts +41 -0
  157. package/dist/routes/judgments.d.ts.map +1 -0
  158. package/dist/routes/judgments.js +566 -0
  159. package/dist/routes/judgments.js.map +1 -0
  160. package/dist/routes/orgs.d.ts +5 -2
  161. package/dist/routes/orgs.d.ts.map +1 -1
  162. package/dist/routes/orgs.js +38 -22
  163. package/dist/routes/orgs.js.map +1 -1
  164. package/dist/routes/policies.d.ts.map +1 -1
  165. package/dist/routes/policies.js +12 -1
  166. package/dist/routes/policies.js.map +1 -1
  167. package/dist/routes/projects.d.ts +10 -2
  168. package/dist/routes/projects.d.ts.map +1 -1
  169. package/dist/routes/projects.js +87 -79
  170. package/dist/routes/projects.js.map +1 -1
  171. package/dist/routes/provenance.d.ts.map +1 -1
  172. package/dist/routes/provenance.js +32 -1
  173. package/dist/routes/provenance.js.map +1 -1
  174. package/dist/routes/providers.d.ts.map +1 -1
  175. package/dist/routes/providers.js +6 -1
  176. package/dist/routes/providers.js.map +1 -1
  177. package/dist/routes/runs.d.ts +0 -7
  178. package/dist/routes/runs.d.ts.map +1 -1
  179. package/dist/routes/runs.js +65 -20
  180. package/dist/routes/runs.js.map +1 -1
  181. package/dist/routes/scope-params.d.ts +16 -1
  182. package/dist/routes/scope-params.d.ts.map +1 -1
  183. package/dist/routes/scope-params.js +28 -0
  184. package/dist/routes/scope-params.js.map +1 -1
  185. package/dist/routes/teams.d.ts +6 -2
  186. package/dist/routes/teams.d.ts.map +1 -1
  187. package/dist/routes/teams.js +77 -73
  188. package/dist/routes/teams.js.map +1 -1
  189. package/dist/types.d.ts +4 -3
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/webhook-endpoint-binding.d.ts +11 -0
  192. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  193. package/dist/webhook-endpoint-binding.js.map +1 -1
  194. package/openapi.json +13316 -9299
  195. package/package.json +21 -21
  196. package/src/agent-binding.ts +19 -4
  197. package/src/agent-pins.ts +147 -0
  198. package/src/app.ts +76 -3
  199. package/src/block-binding.ts +137 -0
  200. package/src/block-pins.ts +148 -0
  201. package/src/cost-binding.ts +116 -5
  202. package/src/deploy-versions.ts +157 -0
  203. package/src/deployment-binding.ts +27 -3
  204. package/src/derive-agent-version.ts +206 -0
  205. package/src/errors.ts +17 -0
  206. package/src/eval-case-binding.ts +71 -0
  207. package/src/eval-run-binding.ts +33 -0
  208. package/src/eval-run-dispatcher.ts +57 -16
  209. package/src/eval-suite-binding.ts +2 -0
  210. package/src/flow-binding.ts +11 -4
  211. package/src/flow-pins.ts +113 -0
  212. package/src/hitl-binding.ts +20 -4
  213. package/src/index.ts +88 -2
  214. package/src/judged-dispatcher.ts +507 -0
  215. package/src/judged-items.ts +263 -0
  216. package/src/judgment-binding.ts +349 -0
  217. package/src/middleware/auth.ts +3 -2
  218. package/src/middleware/idempotency.ts +7 -1
  219. package/src/openapi/generate.ts +7 -1
  220. package/src/openapi/operations.ts +615 -24
  221. package/src/openapi/schemas.ts +1157 -22
  222. package/src/provenance-binding.ts +42 -1
  223. package/src/provider-binding.ts +12 -7
  224. package/src/reviewer-binding.ts +10 -2
  225. package/src/reviewer-role.ts +35 -0
  226. package/src/routes/agents.ts +243 -19
  227. package/src/routes/approvals.ts +70 -19
  228. package/src/routes/blocks.ts +362 -0
  229. package/src/routes/conversations.ts +57 -1
  230. package/src/routes/cost.ts +159 -21
  231. package/src/routes/deployments.ts +266 -56
  232. package/src/routes/eval-comparison.ts +101 -0
  233. package/src/routes/eval-runs.ts +11 -0
  234. package/src/routes/flows.ts +63 -5
  235. package/src/routes/hierarchy-errors.ts +51 -0
  236. package/src/routes/identity.ts +10 -2
  237. package/src/routes/judged-suites.ts +363 -0
  238. package/src/routes/judgment-context.ts +128 -0
  239. package/src/routes/judgment-flow-context.ts +245 -0
  240. package/src/routes/judgments.ts +743 -0
  241. package/src/routes/orgs.ts +44 -27
  242. package/src/routes/policies.ts +19 -0
  243. package/src/routes/projects.ts +106 -95
  244. package/src/routes/provenance.ts +48 -1
  245. package/src/routes/providers.ts +5 -0
  246. package/src/routes/runs.ts +79 -22
  247. package/src/routes/scope-params.ts +35 -1
  248. package/src/routes/teams.ts +96 -90
  249. package/src/types.ts +4 -3
  250. package/src/webhook-endpoint-binding.ts +11 -0
@@ -6,12 +6,15 @@ import { Hono } from 'hono';
6
6
  import type { Cursor, TenantId, Timestamp } from '@kindgi/types';
7
7
 
8
8
  import {
9
+ COST_AGGREGATE_DEFAULT_LIMIT,
10
+ COST_AGGREGATE_MAX_LIMIT,
9
11
  COST_GROUP_DIMENSIONS,
10
12
  type CostAggregateGroup,
11
13
  type CostBinding,
12
14
  type CostGroupDimension,
13
15
  type CostRecord,
14
16
  type CostRecordFilter,
17
+ type CostTokenTotals,
15
18
  } from '../cost-binding.js';
16
19
  import { statusFor, toWireError } from '../errors.js';
17
20
  import type { AppEnv } from '../types.js';
@@ -52,6 +55,11 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
52
55
  c.status(statusFor('bad-input') as never);
53
56
  return c.json(toWireError({ code: 'bad-input', message: filter.error }, requestId));
54
57
  }
58
+ const include = parseInclude(c.req.query('include'));
59
+ if (include.kind === 'err') {
60
+ c.status(statusFor('bad-input') as never);
61
+ return c.json(toWireError({ code: 'bad-input', message: include.error }, requestId));
62
+ }
55
63
 
56
64
  // Thread the ?scopeKind + ?scopeId + ?inherit
57
65
  // triplet as a sibling of `filter` (matches the binding's shape:
@@ -71,6 +79,7 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
71
79
  filter: filter.value,
72
80
  ...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
73
81
  ...(scopeParsed.inherit !== undefined && { inherit: scopeParsed.inherit }),
82
+ ...(include.rawUsage && { includeRawUsage: true }),
74
83
  });
75
84
  return c.json({
76
85
  data: page.data.map(serializeRecord),
@@ -84,8 +93,17 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
84
93
  const requestId = c.get('requestId');
85
94
  const tenantId = c.get('tenantId') as TenantId;
86
95
  const recordId = c.req.param('recordId');
96
+ const include = parseInclude(c.req.query('include'));
97
+ if (include.kind === 'err') {
98
+ c.status(statusFor('bad-input') as never);
99
+ return c.json(toWireError({ code: 'bad-input', message: include.error }, requestId));
100
+ }
87
101
 
88
- const record = await binding.getRecord({ tenantId, recordId });
102
+ const record = await binding.getRecord({
103
+ tenantId,
104
+ recordId,
105
+ ...(include.rawUsage && { includeRawUsage: true }),
106
+ });
89
107
  if (record === null) {
90
108
  c.status(statusFor('cost-record-not-found') as never);
91
109
  return c.json(
@@ -153,6 +171,21 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
153
171
  }
154
172
  }
155
173
 
174
+ // limit — the most groups to return, the most expensive first.
175
+ const limit = parseAggregateLimit(query.limit);
176
+ if (limit === null) {
177
+ c.status(statusFor('bad-input') as never);
178
+ return c.json(
179
+ toWireError(
180
+ {
181
+ code: 'bad-input',
182
+ message: `\`limit\` must be an integer from 1 to ${COST_AGGREGATE_MAX_LIMIT} (default ${COST_AGGREGATE_DEFAULT_LIMIT})`,
183
+ },
184
+ requestId,
185
+ ),
186
+ );
187
+ }
188
+
156
189
  // Time range — required. Default to last 30 days when both absent
157
190
  // (documented in the response `timeRange`). If only one endpoint is
158
191
  // supplied, reject — half-open defaults invite confusion.
@@ -224,12 +257,21 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
224
257
  ...(Object.keys(filter.value).length > 0 && { filter: filter.value }),
225
258
  ...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
226
259
  ...(scopeParsed.inherit !== undefined && { inherit: scopeParsed.inherit }),
260
+ limit,
227
261
  });
228
262
 
263
+ // The most expensive groups, capped here too: a binding may return
264
+ // every group. The window's totals stay over every record.
265
+ const groups = [...result.groups].sort((a, b) => compareGroups(a, b, groupBy)).slice(0, limit);
266
+ const totalGroups = Math.max(result.totalGroups ?? 0, result.groups.length);
267
+
229
268
  return c.json({
230
- groups: result.groups.map(serializeGroup),
269
+ groups: groups.map(serializeGroup),
270
+ totalGroups,
271
+ truncated: totalGroups > groups.length,
231
272
  totalUsd: result.totalUsd,
232
273
  totalRecords: result.totalRecords,
274
+ tokens: serializeTokens(result.tokens),
233
275
  timeRange: {
234
276
  from: result.timeRange.from,
235
277
  to: result.timeRange.to,
@@ -248,27 +290,34 @@ function parseRecordFilter(
248
290
  opts: { skipTime?: boolean } = {},
249
291
  ): { kind: 'ok'; value: CostRecordFilter } | { kind: 'err'; error: string } {
250
292
  const out: {
251
- runId?: string;
252
- agentId?: string;
253
- conversationId?: string;
254
- category?: string;
255
- providerId?: string;
256
- from?: Date;
257
- to?: Date;
293
+ -readonly [K in keyof CostRecordFilter]: CostRecordFilter[K];
258
294
  } = {};
259
- const strKeys: Array<
260
- ['runId' | 'agentId' | 'conversationId' | 'category' | 'providerId', string]
261
- > = [
262
- ['runId', 'runId'],
263
- ['agentId', 'agentId'],
264
- ['conversationId', 'conversationId'],
265
- ['category', 'category'],
266
- ['providerId', 'providerId'],
267
- ];
268
- for (const [outKey, qKey] of strKeys) {
269
- const raw = query[qKey];
295
+ const strKeys = [
296
+ 'runId',
297
+ 'agentId',
298
+ 'conversationId',
299
+ 'category',
300
+ 'providerId',
301
+ 'model',
302
+ 'servedModel',
303
+ 'rootRunId',
304
+ ] as const;
305
+ for (const key of strKeys) {
306
+ const raw = query[key];
270
307
  if (raw !== undefined && raw.length > 0) {
271
- out[outKey] = raw;
308
+ out[key] = raw;
309
+ }
310
+ }
311
+ const descendants = query.includeDescendants;
312
+ if (descendants !== undefined && descendants.length > 0) {
313
+ if (descendants !== 'true' && descendants !== 'false') {
314
+ return { kind: 'err', error: '`includeDescendants` must be `true` or `false`' };
315
+ }
316
+ if (descendants === 'true') {
317
+ if (out.runId === undefined) {
318
+ return { kind: 'err', error: '`includeDescendants` needs a `runId`' };
319
+ }
320
+ out.includeDescendants = true;
272
321
  }
273
322
  }
274
323
  if (opts.skipTime !== true) {
@@ -291,6 +340,22 @@ function parseRecordFilter(
291
340
  return { kind: 'ok', value: out };
292
341
  }
293
342
 
343
+ /** The `include` query: optional extra fields, comma-separated. Only `rawUsage` today. */
344
+ function parseInclude(
345
+ raw: string | undefined,
346
+ ): { kind: 'ok'; rawUsage: boolean } | { kind: 'err'; error: string } {
347
+ const fields = (raw ?? '')
348
+ .split(',')
349
+ .map((f) => f.trim())
350
+ .filter((f) => f.length > 0);
351
+ for (const field of fields) {
352
+ if (field !== 'rawUsage') {
353
+ return { kind: 'err', error: `\`include\` value "${field}" is not known (one of: rawUsage)` };
354
+ }
355
+ }
356
+ return { kind: 'ok', rawUsage: fields.includes('rawUsage') };
357
+ }
358
+
294
359
  function parseIsoDate(raw: string): Date | null {
295
360
  const t = Date.parse(raw);
296
361
  if (!Number.isFinite(t)) return null;
@@ -312,14 +377,87 @@ function serializeRecord(rec: CostRecord): Record<string, unknown> {
312
377
  occurredAt: rec.occurredAt as unknown as string,
313
378
  ...(rec.metrics !== undefined && { metrics: rec.metrics }),
314
379
  ...(rec.attributes !== undefined && { attributes: rec.attributes }),
380
+ ...pick(rec, MODEL_CALL_FIELDS),
315
381
  };
316
382
  }
317
383
 
384
+ /** A model call's fields on the wire, as the binding gives them (all optional). */
385
+ const MODEL_CALL_FIELDS = [
386
+ 'callId',
387
+ 'projectId',
388
+ 'rootRunId',
389
+ 'parentRunId',
390
+ 'agentVersion',
391
+ 'flowId',
392
+ 'nodeId',
393
+ 'step',
394
+ 'purpose',
395
+ 'model',
396
+ 'servedModel',
397
+ 'fallback',
398
+ 'status',
399
+ 'usage',
400
+ 'durationMs',
401
+ 'finishReason',
402
+ 'providerRequestId',
403
+ 'attempts',
404
+ 'error',
405
+ 'rawUsage',
406
+ ] as const satisfies readonly (keyof CostRecord)[];
407
+
408
+ function pick(rec: CostRecord, keys: readonly (keyof CostRecord)[]): Record<string, unknown> {
409
+ const out: Record<string, unknown> = {};
410
+ for (const key of keys) {
411
+ if (rec[key] !== undefined) out[key] = rec[key];
412
+ }
413
+ return out;
414
+ }
415
+
416
+ /** `?limit=` on the aggregate: the default when absent, `null` when it isn't 1..max. */
417
+ function parseAggregateLimit(raw: string | undefined): number | null {
418
+ if (raw === undefined) return COST_AGGREGATE_DEFAULT_LIMIT;
419
+ if (!/^\d+$/.test(raw)) return null;
420
+ const n = Number(raw);
421
+ return n >= 1 && n <= COST_AGGREGATE_MAX_LIMIT ? n : null;
422
+ }
423
+
424
+ /**
425
+ * The aggregate's order: `totalUsd` descending, then the key, dimension
426
+ * by dimension in `groupBy` order (code-point order; `null` last).
427
+ */
428
+ function compareGroups(
429
+ a: CostAggregateGroup,
430
+ b: CostAggregateGroup,
431
+ groupBy: readonly CostGroupDimension[],
432
+ ): number {
433
+ if (a.totalUsd !== b.totalUsd) return b.totalUsd - a.totalUsd;
434
+ for (const dim of groupBy) {
435
+ const x = a.key[dim] ?? null;
436
+ const y = b.key[dim] ?? null;
437
+ if (x === y) continue;
438
+ if (x === null) return 1;
439
+ if (y === null) return -1;
440
+ return x < y ? -1 : 1;
441
+ }
442
+ return 0;
443
+ }
444
+
318
445
  function serializeGroup(g: CostAggregateGroup): Record<string, unknown> {
319
446
  return {
320
447
  key: g.key,
321
448
  count: g.count,
322
449
  totalUsd: g.totalUsd,
450
+ tokens: serializeTokens(g.tokens),
451
+ };
452
+ }
453
+
454
+ function serializeTokens(t: CostTokenTotals): CostTokenTotals {
455
+ return {
456
+ prompt: t.prompt,
457
+ completion: t.completion,
458
+ cacheRead: t.cacheRead,
459
+ cacheWrite: t.cacheWrite,
460
+ reasoning: t.reasoning,
323
461
  };
324
462
  }
325
463
 
@@ -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.