@kindgi/api 0.1.2 → 0.1.3

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 (88) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +2 -1
  3. package/dist/app.js.map +1 -1
  4. package/dist/cost-binding.d.ts +81 -5
  5. package/dist/cost-binding.d.ts.map +1 -1
  6. package/dist/cost-binding.js +6 -0
  7. package/dist/cost-binding.js.map +1 -1
  8. package/dist/hitl-binding.d.ts +20 -5
  9. package/dist/hitl-binding.d.ts.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware/auth.d.ts +3 -2
  14. package/dist/middleware/auth.d.ts.map +1 -1
  15. package/dist/middleware/auth.js.map +1 -1
  16. package/dist/middleware/idempotency.d.ts +5 -1
  17. package/dist/middleware/idempotency.d.ts.map +1 -1
  18. package/dist/middleware/idempotency.js +8 -1
  19. package/dist/middleware/idempotency.js.map +1 -1
  20. package/dist/openapi/operations.d.ts.map +1 -1
  21. package/dist/openapi/operations.js +83 -18
  22. package/dist/openapi/operations.js.map +1 -1
  23. package/dist/openapi/schemas.d.ts +18 -0
  24. package/dist/openapi/schemas.d.ts.map +1 -1
  25. package/dist/openapi/schemas.js +202 -7
  26. package/dist/openapi/schemas.js.map +1 -1
  27. package/dist/provenance-binding.d.ts +27 -1
  28. package/dist/provenance-binding.d.ts.map +1 -1
  29. package/dist/provenance-binding.js.map +1 -1
  30. package/dist/reviewer-binding.d.ts +10 -2
  31. package/dist/reviewer-binding.d.ts.map +1 -1
  32. package/dist/reviewer-role.d.ts +13 -0
  33. package/dist/reviewer-role.d.ts.map +1 -0
  34. package/dist/reviewer-role.js +27 -0
  35. package/dist/reviewer-role.js.map +1 -0
  36. package/dist/routes/approvals.d.ts +5 -4
  37. package/dist/routes/approvals.d.ts.map +1 -1
  38. package/dist/routes/approvals.js +52 -16
  39. package/dist/routes/approvals.js.map +1 -1
  40. package/dist/routes/conversations.d.ts +7 -1
  41. package/dist/routes/conversations.d.ts.map +1 -1
  42. package/dist/routes/conversations.js +41 -1
  43. package/dist/routes/conversations.js.map +1 -1
  44. package/dist/routes/cost.d.ts.map +1 -1
  45. package/dist/routes/cost.js +95 -9
  46. package/dist/routes/cost.js.map +1 -1
  47. package/dist/routes/identity.d.ts +7 -0
  48. package/dist/routes/identity.d.ts.map +1 -1
  49. package/dist/routes/identity.js +3 -2
  50. package/dist/routes/identity.js.map +1 -1
  51. package/dist/routes/provenance.d.ts.map +1 -1
  52. package/dist/routes/provenance.js +32 -1
  53. package/dist/routes/provenance.js.map +1 -1
  54. package/dist/routes/runs.d.ts +0 -7
  55. package/dist/routes/runs.d.ts.map +1 -1
  56. package/dist/routes/runs.js +44 -20
  57. package/dist/routes/runs.js.map +1 -1
  58. package/dist/routes/scope-params.d.ts +16 -1
  59. package/dist/routes/scope-params.d.ts.map +1 -1
  60. package/dist/routes/scope-params.js +28 -0
  61. package/dist/routes/scope-params.js.map +1 -1
  62. package/dist/types.d.ts +4 -3
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/webhook-endpoint-binding.d.ts +11 -0
  65. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  66. package/dist/webhook-endpoint-binding.js.map +1 -1
  67. package/openapi.json +599 -24
  68. package/package.json +21 -21
  69. package/src/app.ts +5 -1
  70. package/src/cost-binding.ts +96 -5
  71. package/src/hitl-binding.ts +20 -4
  72. package/src/index.ts +3 -0
  73. package/src/middleware/auth.ts +3 -2
  74. package/src/middleware/idempotency.ts +7 -1
  75. package/src/openapi/operations.ts +93 -18
  76. package/src/openapi/schemas.ts +221 -7
  77. package/src/provenance-binding.ts +42 -1
  78. package/src/reviewer-binding.ts +10 -2
  79. package/src/reviewer-role.ts +35 -0
  80. package/src/routes/approvals.ts +70 -19
  81. package/src/routes/conversations.ts +57 -1
  82. package/src/routes/cost.ts +104 -20
  83. package/src/routes/identity.ts +10 -2
  84. package/src/routes/provenance.ts +48 -1
  85. package/src/routes/runs.ts +54 -22
  86. package/src/routes/scope-params.ts +35 -1
  87. package/src/types.ts +4 -3
  88. 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,8 @@ 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.
637
658
  */
638
659
  function serializeRun(
639
660
  row: KernelRunRecord,
@@ -654,6 +675,13 @@ function serializeRun(
654
675
  ...(opts.output && row.output !== undefined && { output: row.output }),
655
676
  ...(row.parentRunId != null && { parentRunId: row.parentRunId as unknown as string }),
656
677
  ...(row.parentNodeId != null && { parentNodeId: row.parentNodeId as unknown as string }),
678
+ ...(row.agent !== undefined && {
679
+ agent: {
680
+ id: row.agent.id,
681
+ version: row.agent.version,
682
+ conversationId: row.agent.conversationId as unknown as string,
683
+ },
684
+ }),
657
685
  };
658
686
  }
659
687
 
@@ -698,7 +726,7 @@ async function readableRun(
698
726
  /** The binding query for `GET /v1/runs`: scope, page, and the parent filters. */
699
727
  function listRunsInput(input: {
700
728
  readonly tenantId: TenantId;
701
- readonly scope: Extract<ParseScopeParamsOutcome, { kind: 'ok' }>['scope'];
729
+ readonly scope: ListScope | undefined;
702
730
  readonly limit: number;
703
731
  readonly cursor: DecodedCursor | null;
704
732
  readonly filter: RunListFilter;
@@ -706,38 +734,41 @@ function listRunsInput(input: {
706
734
  const { tenantId, scope, limit, cursor, filter } = input;
707
735
  return {
708
736
  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
- }),
737
+ ...(scope !== undefined && { scope }),
715
738
  limit,
716
739
  ...(cursor !== null && {
717
740
  cursor: { createdAt: cursor.createdAt as never, id: cursor.id as RunId },
718
741
  }),
719
742
  ...(filter.parentRunId !== undefined && { parent: { runId: filter.parentRunId } }),
720
743
  ...(filter.topLevelOnly && { topLevelOnly: true }),
744
+ ...(filter.agentId !== undefined && { agentId: filter.agentId }),
721
745
  };
722
746
  }
723
747
 
724
748
  interface RunListFilter {
725
749
  readonly parentRunId?: RunId;
726
750
  readonly topLevelOnly: boolean;
751
+ readonly agentId?: string;
727
752
  readonly includeOutput: boolean;
728
753
  }
729
754
 
730
- /** `?parentRunId=` (children of a run), `?topLevel=true`, `?include=output`. */
755
+ /**
756
+ * `?parentRunId=` (children of a run), `?topLevel=true`, `?agentId=` (an
757
+ * agent's turns), `?include=output`.
758
+ */
731
759
  function parseRunListFilter(
732
760
  query: Readonly<Record<string, string>>,
733
761
  ): { kind: 'ok'; value: RunListFilter } | { kind: 'err'; message: string } {
734
- const { parentRunId, topLevel, include } = query;
762
+ const { parentRunId, topLevel, agentId, include } = query;
763
+ if (agentId !== undefined && agentId.trim() === '') {
764
+ return { kind: 'err', message: '`agentId` must not be empty' };
765
+ }
735
766
  if (topLevel !== undefined && topLevel !== 'true' && topLevel !== 'false') {
736
767
  return { kind: 'err', message: '`topLevel` must be `true` or `false`' };
737
768
  }
738
769
  const topLevelOnly = topLevel === 'true';
739
- if (parentRunId !== undefined && parentRunId.length === 0) {
740
- return { kind: 'err', message: '`parentRunId` must be a run id' };
770
+ if (parentRunId !== undefined && !RUN_ID_RE.test(parentRunId)) {
771
+ return { kind: 'err', message: '`parentRunId` must be a run id (a UUID)' };
741
772
  }
742
773
  if (parentRunId !== undefined && topLevelOnly) {
743
774
  return { kind: 'err', message: '`parentRunId` and `topLevel=true` cannot be combined' };
@@ -752,6 +783,7 @@ function parseRunListFilter(
752
783
  value: {
753
784
  ...(parentRunId !== undefined && { parentRunId: parentRunId as RunId }),
754
785
  topLevelOnly,
786
+ ...(agentId !== undefined && { agentId }),
755
787
  includeOutput: includes.includes('output'),
756
788
  },
757
789
  };
@@ -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
+ }
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 {