@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
@@ -2,10 +2,11 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import { Hono } from 'hono';
5
+ import type { MiddlewareHandler } from 'hono';
5
6
 
6
7
  import type { AgentId } from '@kindgi/agents';
7
8
  import type { KernelRunRecord, ListRunsInput, RunBinding } from '@kindgi/runtime';
8
- import type { FlowId, OrgId, ProjectId, RunId, Semver, TenantId } from '@kindgi/types';
9
+ import type { FlowId, ListScope, ProjectId, RunId, Semver, TenantId } from '@kindgi/types';
9
10
 
10
11
  import { ref } from '@kindgi/authz';
11
12
 
@@ -21,7 +22,7 @@ import type { MintPublicRunTokenResult } from '../public-run-token.js';
21
22
  import type { AppEnv } from '../types.js';
22
23
  import type { DecodedCursor } from './pagination.js';
23
24
  import { clampLimit, decodeCursor } from './pagination.js';
24
- import { type ParseScopeParamsOutcome, parseScopeParams } from './scope-params.js';
25
+ import { parseListScope } from './scope-params.js';
25
26
  import {
26
27
  formatSseFrame,
27
28
  isTerminalWireKind,
@@ -60,6 +61,24 @@ const MAX_RUN_ANCESTRY = 16;
60
61
  * `POST /:runId/cancel`, `POST /:runId/resume`, `GET /:runId/stream`
61
62
  * (SSE), `GET /:runId/journal`.
62
63
  */
64
+ /** A run id: a UUID. */
65
+ const RUN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
+
67
+ /**
68
+ * A `:runId` that isn't a run id is a 400, before it reaches a query (where
69
+ * Postgres's uuid cast would fail it as a 500).
70
+ */
71
+ const refuseMalformedRunId: MiddlewareHandler<AppEnv> = async (c, next) => {
72
+ if (RUN_ID_RE.test(c.req.param('runId') ?? '')) return next();
73
+ c.status(statusFor('bad-input') as never);
74
+ return c.json(
75
+ toWireError(
76
+ { code: 'bad-input', message: '`runId` must be a run id (a UUID)' },
77
+ c.get('requestId'),
78
+ ),
79
+ );
80
+ };
81
+
63
82
  export function runsRouter(
64
83
  binding: RunHandlerBinding,
65
84
  runBinding: RunBinding,
@@ -179,7 +198,7 @@ export function runsRouter(
179
198
  });
180
199
 
181
200
  // ---------- GET /:runId ----------
182
- r.get('/:runId', async (c) => {
201
+ r.get('/:runId', refuseMalformedRunId, async (c) => {
183
202
  const requestId = c.get('requestId');
184
203
  const runId = c.req.param('runId') as RunId;
185
204
 
@@ -194,7 +213,7 @@ export function runsRouter(
194
213
  });
195
214
 
196
215
  // ---------- GET /:runId/progress ----------
197
- r.get('/:runId/progress', async (c) => {
216
+ r.get('/:runId/progress', refuseMalformedRunId, async (c) => {
198
217
  const requestId = c.get('requestId');
199
218
  const runId = c.req.param('runId') as RunId;
200
219
  const loaded = await readableRun(runBinding, c, runId);
@@ -209,7 +228,8 @@ export function runsRouter(
209
228
 
210
229
  // ---------- GET / (list, cursor-paginated) ----------
211
230
  //
212
- // Content-scoped list; scope filter threaded via `parseScopeParams`.
231
+ // Content-scoped list; scope filter threaded via `parseListScope` (a
232
+ // `scopeId` that isn't a UUID is a 400 here, not a failed query).
213
233
  // Every run belongs to exactly one project; `scope.kind === 'project'`
214
234
  // narrows to that project, `scope.kind === 'org'` to the org's
215
235
  // projects, and tenant / undefined behave as documented in
@@ -232,7 +252,7 @@ export function runsRouter(
232
252
  cursorFilter = decoded;
233
253
  }
234
254
 
235
- const scopeParsed = parseScopeParams(c.req.query(), { tenantId });
255
+ const scopeParsed = parseListScope(c.req.query(), { tenantId });
236
256
  if (scopeParsed.kind === 'err') {
237
257
  c.status(statusFor('scope-invalid') as never);
238
258
  return c.json(
@@ -263,7 +283,7 @@ export function runsRouter(
263
283
  });
264
284
 
265
285
  // ---------- POST /:runId/cancel ----------
266
- r.post('/:runId/cancel', async (c) => {
286
+ r.post('/:runId/cancel', refuseMalformedRunId, async (c) => {
267
287
  const requestId = c.get('requestId');
268
288
  const tenantId = c.get('tenantId') as TenantId;
269
289
  const runId = c.req.param('runId') as RunId;
@@ -562,11 +582,11 @@ export function runsRouter(
562
582
  },
563
583
  });
564
584
  };
565
- r.get('/:runId/stream', (c) => streamRun(c, 'full'));
566
- r.get('/:runId/progress/stream', (c) => streamRun(c, 'progress'));
585
+ r.get('/:runId/stream', refuseMalformedRunId, (c) => streamRun(c, 'full'));
586
+ r.get('/:runId/progress/stream', refuseMalformedRunId, (c) => streamRun(c, 'progress'));
567
587
 
568
588
  // ---------- GET /:runId/journal ----------
569
- r.get('/:runId/journal', async (c) => {
589
+ r.get('/:runId/journal', refuseMalformedRunId, async (c) => {
570
590
  const requestId = c.get('requestId');
571
591
  const tenantId = c.get('tenantId') as TenantId;
572
592
  const runId = c.req.param('runId') as RunId;
@@ -633,7 +653,9 @@ function invokeFromBody(
633
653
  /**
634
654
  * Wire row for a run. `output` is the run's output once it completed;
635
655
  * single-run responses carry it, lists only with `?include=output`
636
- * (outputs can be large). Child runs carry their parent's run + node.
656
+ * (outputs can be large). Child runs carry their parent's run + node;
657
+ * an agent's turns, the agent, its version and the conversation; a
658
+ * replay run, the run it replays and its eval run.
637
659
  */
638
660
  function serializeRun(
639
661
  row: KernelRunRecord,
@@ -654,6 +676,15 @@ function serializeRun(
654
676
  ...(opts.output && row.output !== undefined && { output: row.output }),
655
677
  ...(row.parentRunId != null && { parentRunId: row.parentRunId as unknown as string }),
656
678
  ...(row.parentNodeId != null && { parentNodeId: row.parentNodeId as unknown as string }),
679
+ ...(row.agent !== undefined && {
680
+ agent: {
681
+ id: row.agent.id,
682
+ version: row.agent.version,
683
+ conversationId: row.agent.conversationId as unknown as string,
684
+ },
685
+ }),
686
+ ...(row.replayOf != null && { replayOf: row.replayOf as unknown as string }),
687
+ ...(row.evalRunId != null && { evalRunId: row.evalRunId }),
657
688
  };
658
689
  }
659
690
 
@@ -698,7 +729,7 @@ async function readableRun(
698
729
  /** The binding query for `GET /v1/runs`: scope, page, and the parent filters. */
699
730
  function listRunsInput(input: {
700
731
  readonly tenantId: TenantId;
701
- readonly scope: Extract<ParseScopeParamsOutcome, { kind: 'ok' }>['scope'];
732
+ readonly scope: ListScope | undefined;
702
733
  readonly limit: number;
703
734
  readonly cursor: DecodedCursor | null;
704
735
  readonly filter: RunListFilter;
@@ -706,42 +737,65 @@ function listRunsInput(input: {
706
737
  const { tenantId, scope, limit, cursor, filter } = input;
707
738
  return {
708
739
  tenantId,
709
- ...(scope?.kind === 'project' && {
710
- scope: { kind: 'project' as const, projectId: scope.projectId as ProjectId },
711
- }),
712
- ...(scope?.kind === 'org' && {
713
- scope: { kind: 'org' as const, orgId: scope.orgId as unknown as OrgId },
714
- }),
740
+ ...(scope !== undefined && { scope }),
715
741
  limit,
716
742
  ...(cursor !== null && {
717
743
  cursor: { createdAt: cursor.createdAt as never, id: cursor.id as RunId },
718
744
  }),
719
745
  ...(filter.parentRunId !== undefined && { parent: { runId: filter.parentRunId } }),
720
746
  ...(filter.topLevelOnly && { topLevelOnly: true }),
747
+ ...(filter.agentId !== undefined && { agentId: filter.agentId }),
748
+ replays: filter.replays,
749
+ ...(filter.evalRunId !== undefined && { evalRunId: filter.evalRunId }),
721
750
  };
722
751
  }
723
752
 
724
753
  interface RunListFilter {
725
754
  readonly parentRunId?: RunId;
726
755
  readonly topLevelOnly: boolean;
756
+ readonly agentId?: string;
757
+ readonly replays: 'exclude' | 'include' | 'only';
758
+ readonly evalRunId?: string;
727
759
  readonly includeOutput: boolean;
728
760
  }
729
761
 
730
- /** `?parentRunId=` (children of a run), `?topLevel=true`, `?include=output`. */
762
+ /**
763
+ * `?parentRunId=` (children of a run), `?topLevel=true`, `?agentId=` (an
764
+ * agent's turns), `?replays=exclude|include|only` (default `exclude`),
765
+ * `?evalRunId=` (one eval run's replays; implies they are included),
766
+ * `?include=output`.
767
+ */
731
768
  function parseRunListFilter(
732
769
  query: Readonly<Record<string, string>>,
733
770
  ): { kind: 'ok'; value: RunListFilter } | { kind: 'err'; message: string } {
734
- const { parentRunId, topLevel, include } = query;
771
+ const { parentRunId, topLevel, agentId, replays, evalRunId, include } = query;
772
+ if (agentId !== undefined && agentId.trim() === '') {
773
+ return { kind: 'err', message: '`agentId` must not be empty' };
774
+ }
735
775
  if (topLevel !== undefined && topLevel !== 'true' && topLevel !== 'false') {
736
776
  return { kind: 'err', message: '`topLevel` must be `true` or `false`' };
737
777
  }
738
778
  const topLevelOnly = topLevel === 'true';
739
- if (parentRunId !== undefined && parentRunId.length === 0) {
740
- return { kind: 'err', message: '`parentRunId` must be a run id' };
779
+ if (parentRunId !== undefined && !RUN_ID_RE.test(parentRunId)) {
780
+ return { kind: 'err', message: '`parentRunId` must be a run id (a UUID)' };
741
781
  }
742
782
  if (parentRunId !== undefined && topLevelOnly) {
743
783
  return { kind: 'err', message: '`parentRunId` and `topLevel=true` cannot be combined' };
744
784
  }
785
+ if (
786
+ replays !== undefined &&
787
+ replays !== 'exclude' &&
788
+ replays !== 'include' &&
789
+ replays !== 'only'
790
+ ) {
791
+ return { kind: 'err', message: '`replays` must be `exclude`, `include` or `only`' };
792
+ }
793
+ if (evalRunId !== undefined && evalRunId.trim() === '') {
794
+ return { kind: 'err', message: '`evalRunId` must not be empty' };
795
+ }
796
+ if (evalRunId !== undefined && replays === 'exclude') {
797
+ return { kind: 'err', message: '`evalRunId` and `replays=exclude` cannot be combined' };
798
+ }
745
799
  const includes = include === undefined ? [] : include.split(',').map((i) => i.trim());
746
800
  const unknown = includes.filter((i) => i !== 'output');
747
801
  if (unknown.length > 0) {
@@ -752,6 +806,9 @@ function parseRunListFilter(
752
806
  value: {
753
807
  ...(parentRunId !== undefined && { parentRunId: parentRunId as RunId }),
754
808
  topLevelOnly,
809
+ ...(agentId !== undefined && { agentId }),
810
+ replays: replays ?? (evalRunId !== undefined ? 'include' : 'exclude'),
811
+ ...(evalRunId !== undefined && { evalRunId }),
755
812
  includeOutput: includes.includes('output'),
756
813
  },
757
814
  };
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { type ResourceRef, ref } from '@kindgi/authz';
5
5
  import type { Scope } from '@kindgi/platform';
6
- import type { OrgId, ProjectId, TenantId } from '@kindgi/types';
6
+ import type { ListScope, OrgId, ProjectId, TenantId } from '@kindgi/types';
7
7
 
8
8
  /**
9
9
  * The authorization resource for a request scope: the project or org it
@@ -131,3 +131,37 @@ export function parseScopeParams(
131
131
 
132
132
  return { kind: 'ok', scope, ...(inherit !== undefined && { inherit }) };
133
133
  }
134
+
135
+ const SCOPE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
136
+
137
+ /**
138
+ * A content list's `?scopeKind` + `?scopeId` as a `ListScope`: a project
139
+ * or an org narrows the list; no scope, or `tenant`, is the whole tenant.
140
+ * Project and org ids are UUIDs, so a malformed `scopeId` is an error
141
+ * here (the route's 400), not a failed query.
142
+ */
143
+ export function parseListScope(
144
+ query: QuerySource,
145
+ session: { readonly tenantId: TenantId },
146
+ ):
147
+ | { readonly kind: 'ok'; readonly scope?: ListScope }
148
+ | { readonly kind: 'err'; readonly message: string } {
149
+ const parsed = parseScopeParams(query, session);
150
+ if (parsed.kind === 'err') return parsed;
151
+ const scope = parsed.scope;
152
+ if (scope === undefined || scope.kind === 'tenant') return { kind: 'ok' };
153
+ const id = (scope.kind === 'project' ? scope.projectId : scope.orgId) as unknown as string;
154
+ if (!SCOPE_ID_RE.test(id)) {
155
+ return {
156
+ kind: 'err',
157
+ message: `scope query parameters malformed: scopeId must be ${scope.kind === 'project' ? 'a project' : 'an org'} id (a UUID), got "${id}"`,
158
+ };
159
+ }
160
+ return {
161
+ kind: 'ok',
162
+ scope:
163
+ scope.kind === 'project'
164
+ ? { kind: 'project', projectId: scope.projectId }
165
+ : { kind: 'org', orgId: scope.orgId },
166
+ };
167
+ }
@@ -19,6 +19,7 @@ import type { Cursor, OrgId, TeamId, TenantId, UserId } from '@kindgi/types';
19
19
  import { statusFor, toWireError } from '../errors.js';
20
20
  import type { Authorizer } from '../middleware/authorize.js';
21
21
  import type { AppEnv } from '../types.js';
22
+ import { slugConflictError } from './hierarchy-errors.js';
22
23
  import { clampLimit } from './pagination.js';
23
24
 
24
25
  /**
@@ -29,9 +30,13 @@ import { clampLimit } from './pagination.js';
29
30
  * `?limit=` / `?cursor=` / `?orgId=` /
30
31
  * `?nameContains=`.
31
32
  * - `POST /` — create; body `{ name, slug,
32
- * description?, orgId? }`; 201 `{ id }`.
33
+ * description?, orgId? }`; 201 `{ id }`;
34
+ * 409 `slug-conflict` when the tenant
35
+ * has a team with that slug.
33
36
  * - `GET /:teamId` — get; 200 or 404 `team-not-found`.
34
- * - `PATCH /:teamId` — partial update; 204 or 404.
37
+ * - `PATCH /:teamId` — partial update; 204, 404
38
+ * `team-not-found` or 409
39
+ * `slug-conflict`.
35
40
  * - `DELETE /:teamId` — 204 idempotent.
36
41
  * - `GET /:teamId/memberships` — list; cursor-paginated.
37
42
  * - `POST /:teamId/memberships` — add member; body `{ userId, role }`;
@@ -193,17 +198,24 @@ export function teamsRouter(
193
198
  : ('00000000-0000-0000-0000-000000000000' as UserId);
194
199
  const result = await tenantHierarchy.createTeam({ tenantId, creatorUserId, spec });
195
200
  if (result.kind === 'err') {
196
- const code =
197
- result.error.code === 'slug-conflict' ? 'slug-conflict' : 'internal-server-error';
198
- c.status(statusFor(code) as never);
199
- return c.json(toWireError({ code, message: result.error.message }, requestId));
201
+ const error =
202
+ result.error.code === 'slug-conflict'
203
+ ? slugConflictError('team', spec.slug)
204
+ : { code: 'internal-server-error', message: result.error.message };
205
+ c.status(statusFor(error.code) as never);
206
+ return c.json(toWireError(error, requestId));
200
207
  }
201
208
  c.status(201);
202
209
  return c.json({ id: result.value.teamId as unknown as string });
203
210
  }
204
- const id = await binding.create(tenantId, spec);
211
+ const outcome = await binding.create(tenantId, spec);
212
+ if (outcome.kind === 'slug-conflict') {
213
+ const error = slugConflictError('team', outcome.slug);
214
+ c.status(statusFor(error.code) as never);
215
+ return c.json(toWireError(error, requestId));
216
+ }
205
217
  c.status(201);
206
- return c.json({ id: id as unknown as string });
218
+ return c.json({ id: outcome.teamId as unknown as string });
207
219
  });
208
220
 
209
221
  // ---------- GET /:teamId/memberships ----------
@@ -300,39 +312,38 @@ export function teamsRouter(
300
312
  ),
301
313
  );
302
314
  }
303
- try {
304
- if (authorizer !== undefined) {
305
- const res = await tenantHierarchy.addTeamMember({
306
- tenantId,
307
- teamId,
308
- userId: b.userId as unknown as UserId,
309
- role: b.role as TeamRole,
310
- });
311
- if (res.kind === 'err') throw new Error(res.error.message);
312
- } else {
313
- await membershipBinding.add(tenantId, {
314
- teamId,
315
- userId: b.userId as unknown as UserId,
316
- role: b.role,
317
- });
318
- }
319
- } catch (cause) {
320
- const message = cause instanceof Error ? cause.message : String(cause);
321
- if (message.startsWith('team-not-found')) {
322
- // Racy — team was deleted between the preliminary get and the add.
323
- c.status(statusFor('team-not-found') as never);
324
- return c.json(
325
- toWireError(
326
- {
327
- code: 'team-not-found',
328
- message: `No team with id "${teamId as unknown as string}"`,
329
- teamId: teamId as unknown as string,
330
- },
331
- requestId,
332
- ),
333
- );
315
+ // Racy — the team can be deleted between the preliminary get and the add.
316
+ const teamGone = () => {
317
+ c.status(statusFor('team-not-found') as never);
318
+ return c.json(
319
+ toWireError(
320
+ {
321
+ code: 'team-not-found',
322
+ message: `No team with id "${teamId as unknown as string}"`,
323
+ teamId: teamId as unknown as string,
324
+ },
325
+ requestId,
326
+ ),
327
+ );
328
+ };
329
+ if (authorizer !== undefined) {
330
+ const res = await tenantHierarchy.addTeamMember({
331
+ tenantId,
332
+ teamId,
333
+ userId: b.userId as unknown as UserId,
334
+ role: b.role as TeamRole,
335
+ });
336
+ if (res.kind === 'err') {
337
+ if (res.error.code === 'team-not-found') return teamGone();
338
+ throw new Error(res.error.message, { cause: res.error });
334
339
  }
335
- throw cause;
340
+ } else {
341
+ const outcome = await membershipBinding.add(tenantId, {
342
+ teamId,
343
+ userId: b.userId as unknown as UserId,
344
+ role: b.role,
345
+ });
346
+ if (outcome.kind === 'team-not-found') return teamGone();
336
347
  }
337
348
  c.status(201);
338
349
  return c.json({
@@ -387,38 +398,33 @@ export function teamsRouter(
387
398
  ),
388
399
  );
389
400
  }
390
- try {
391
- await membershipBinding.updateRole(tenantId, teamId, userId, b.role);
392
- } catch (cause) {
393
- const message = cause instanceof Error ? cause.message : String(cause);
394
- if (message.startsWith('team-membership-not-found')) {
395
- c.status(statusFor('team-membership-not-found') as never);
396
- return c.json(
397
- toWireError(
398
- {
399
- code: 'team-membership-not-found',
400
- message: `No membership for team "${teamId as unknown as string}" + user "${userId as unknown as string}"`,
401
- teamId: teamId as unknown as string,
402
- userId: userId as unknown as string,
403
- },
404
- requestId,
405
- ),
406
- );
407
- }
408
- if (message.startsWith('team-not-found')) {
409
- c.status(statusFor('team-not-found') as never);
410
- return c.json(
411
- toWireError(
412
- {
413
- code: 'team-not-found',
414
- message: `No team with id "${teamId as unknown as string}"`,
415
- teamId: teamId as unknown as string,
416
- },
417
- requestId,
418
- ),
419
- );
420
- }
421
- throw cause;
401
+ const outcome = await membershipBinding.updateRole(tenantId, teamId, userId, b.role);
402
+ if (outcome.kind === 'team-membership-not-found') {
403
+ c.status(statusFor('team-membership-not-found') as never);
404
+ return c.json(
405
+ toWireError(
406
+ {
407
+ code: 'team-membership-not-found',
408
+ message: `No membership for team "${teamId as unknown as string}" + user "${userId as unknown as string}"`,
409
+ teamId: teamId as unknown as string,
410
+ userId: userId as unknown as string,
411
+ },
412
+ requestId,
413
+ ),
414
+ );
415
+ }
416
+ if (outcome.kind === 'team-not-found') {
417
+ c.status(statusFor('team-not-found') as never);
418
+ return c.json(
419
+ toWireError(
420
+ {
421
+ code: 'team-not-found',
422
+ message: `No team with id "${teamId as unknown as string}"`,
423
+ teamId: teamId as unknown as string,
424
+ },
425
+ requestId,
426
+ ),
427
+ );
422
428
  }
423
429
  c.status(204);
424
430
  return c.body(null);
@@ -522,24 +528,24 @@ export function teamsRouter(
522
528
  );
523
529
  }
524
530
  }
525
- try {
526
- await binding.update(tenantId, teamId, shaped);
527
- } catch (cause) {
528
- const message = cause instanceof Error ? cause.message : String(cause);
529
- if (message.startsWith('team-not-found')) {
530
- c.status(statusFor('team-not-found') as never);
531
- return c.json(
532
- toWireError(
533
- {
534
- code: 'team-not-found',
535
- message: `No team with id "${teamId as unknown as string}"`,
536
- teamId: teamId as unknown as string,
537
- },
538
- requestId,
539
- ),
540
- );
541
- }
542
- throw cause;
531
+ const outcome = await binding.update(tenantId, teamId, shaped);
532
+ if (outcome.kind === 'team-not-found') {
533
+ c.status(statusFor('team-not-found') as never);
534
+ return c.json(
535
+ toWireError(
536
+ {
537
+ code: 'team-not-found',
538
+ message: `No team with id "${teamId as unknown as string}"`,
539
+ teamId: teamId as unknown as string,
540
+ },
541
+ requestId,
542
+ ),
543
+ );
544
+ }
545
+ if (outcome.kind === 'slug-conflict') {
546
+ const error = slugConflictError('team', outcome.slug);
547
+ c.status(statusFor(error.code) as never);
548
+ return c.json(toWireError(error, requestId));
543
549
  }
544
550
  c.status(204);
545
551
  return c.body(null);
package/src/types.ts CHANGED
@@ -15,9 +15,10 @@ export interface AppEnv {
15
15
  tenantId: TenantId;
16
16
  /**
17
17
  * Set by `bearerAuthMiddleware` when the token's `TokenResolution`
18
- * carried a `reviewerRole`. Approvals routes gate visibility +
19
- * decisions on this: `standard < senior < admin`. Absent for
20
- * tokens that were never provisioned with a reviewer role.
18
+ * carried a `reviewerRole`, or by the approvals routes (and `whoami`)
19
+ * from the reviewer roster for a token whose user is a registered
20
+ * reviewer (`ReviewerBinding.resolveReviewerRole`). Approvals routes
21
+ * gate visibility + decisions on this: `standard < senior < admin`.
21
22
  */
22
23
  reviewerRole?: ReviewerRole;
23
24
  /**
@@ -12,6 +12,7 @@ import type {
12
12
  WebhookEndpointId,
13
13
  WebhookEventId,
14
14
  } from '@kindgi/types';
15
+ import type { CostTokenTotals } from './cost-binding.js';
15
16
 
16
17
  /**
17
18
  * Outbound webhook endpoints: URLs the platform sends signed events to,
@@ -175,6 +176,16 @@ export interface FinishedRun {
175
176
  readonly failureMessage: string | null;
176
177
  readonly createdAt: Timestamp;
177
178
  readonly completedAt: Timestamp;
179
+ /**
180
+ * The model calls of the whole run tree (this run and every run it
181
+ * started), from the cost ledger, summed when the run ended. Absent
182
+ * when the runtime records no usage.
183
+ */
184
+ readonly usage?: {
185
+ readonly calls: number;
186
+ readonly costUsd: number;
187
+ readonly tokens: CostTokenTotals;
188
+ };
178
189
  }
179
190
 
180
191
  export interface RunFinishedEvent {