@tai42/api-client 12.0.0 → 13.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -498,6 +498,66 @@ export interface SetMcpSecretEnvBody {
498
498
  readonly manifest_pointer: string;
499
499
  }
500
500
 
501
+ /**
502
+ * The client-facing write body for a state declaration (`PUT /api/states/{name}`), the
503
+ * wire shape of `tai42_contract.states.StateDeclaration` minus the platform-computed
504
+ * `effective_schema`/`regimes` (those are read-only, refused on a write). `retention_days`
505
+ * is `null` to keep records forever.
506
+ */
507
+ export interface StateDeclarationBody {
508
+ readonly name: string;
509
+ readonly description?: string;
510
+ readonly schema: Record<string, unknown>;
511
+ readonly subject_kinds: readonly string[];
512
+ readonly default_subject_kind: string;
513
+ readonly retention_days?: number | null;
514
+ }
515
+
516
+ /** A state-module document write body (`PUT /api/state-modules/{name}`, the platform half). */
517
+ export interface StateModuleBody {
518
+ readonly name: string;
519
+ readonly description?: string;
520
+ readonly parameters?: Record<string, unknown>;
521
+ readonly schema: Record<string, unknown>;
522
+ readonly regimes?: Record<string, unknown>[];
523
+ readonly declarations?: Record<string, unknown> | null;
524
+ readonly trace?: Record<string, unknown>;
525
+ }
526
+
527
+ /** A module mount / re-mount body (`PUT|PATCH /api/states/{name}/mounts/{module}`). */
528
+ export interface StateMountBody {
529
+ readonly path?: string[];
530
+ readonly parameters?: Record<string, unknown>;
531
+ readonly declarations?: Record<string, unknown>;
532
+ }
533
+
534
+ /** The four-part subject a record route addresses in its path. */
535
+ export interface StateSubjectRef {
536
+ readonly target_kind: string;
537
+ readonly target_name: string;
538
+ readonly kind: string;
539
+ readonly key: string;
540
+ }
541
+
542
+ /** A keyset page request: an optional size and the opaque cursor a prior page returned. */
543
+ export interface StatePageQuery {
544
+ readonly limit?: number;
545
+ readonly cursor?: string;
546
+ }
547
+
548
+ /**
549
+ * The subject-addressed record path: `/api/states/{name}/records/{target_kind}/
550
+ * {target_name}/{kind}/{key}` — four path segments, each percent-encoded, so a key
551
+ * carrying a slash or a colon never escapes its segment.
552
+ */
553
+ function stateRecordPath(name: string, subject: StateSubjectRef): string {
554
+ return (
555
+ `/api/states/${encodeSegment(name)}/records/` +
556
+ `${encodeSegment(subject.target_kind)}/${encodeSegment(subject.target_name)}/` +
557
+ `${encodeSegment(subject.kind)}/${encodeSegment(subject.key)}`
558
+ );
559
+ }
560
+
501
561
  export function createApiClient(config: ApiConfig) {
502
562
  const req = <S extends Parameters<typeof apiRequest>[2]>(
503
563
  path: string,
@@ -606,6 +666,130 @@ export function createApiClient(config: ApiConfig) {
606
666
  { method: 'PUT', body: { tags } },
607
667
  ),
608
668
 
669
+ // -- states --------------------------------------------------------------
670
+ // A state is a declared JSON document, one per subject. The list is the
671
+ // management population; a single state read composes the declaration with its
672
+ // effective schema, regimes and mounts. Every record route addresses its subject
673
+ // as four percent-encoded path segments (see `stateRecordPath`).
674
+ listStates: (signal?: AbortSignal) => req('/api/states', s.stateList, { signal }),
675
+ getState: (name: string, signal?: AbortSignal) =>
676
+ req(`/api/states/${encodeSegment(name)}`, s.stateDetail, { signal }),
677
+ // A plain declaration upsert — no `replace` flag. With records present the server
678
+ // accepts additive schema changes and refuses with a 409 any change that removes or
679
+ // alters an existing field, or a subject-kind removal that would strand records.
680
+ putState: (name: string, body: StateDeclarationBody) =>
681
+ req(`/api/states/${encodeSegment(name)}`, s.stateDetail, {
682
+ method: 'PUT',
683
+ body,
684
+ }),
685
+ deleteState: (name: string) =>
686
+ req(`/api/states/${encodeSegment(name)}`, s.stateDeleted, { method: 'DELETE' }),
687
+ getStateStats: (name: string, signal?: AbortSignal) =>
688
+ req(`/api/states/${encodeSegment(name)}/stats`, s.stateStats, { signal }),
689
+ // -- state mounts --------------------------------------------------------
690
+ listStateMounts: (name: string, signal?: AbortSignal) =>
691
+ req(`/api/states/${encodeSegment(name)}/mounts`, s.stateMountList, { signal }),
692
+ getStateMount: (name: string, module: string, signal?: AbortSignal) =>
693
+ req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateMount, {
694
+ signal,
695
+ }),
696
+ mountStateModule: (name: string, module: string, body: StateMountBody) =>
697
+ req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateMounted, {
698
+ method: 'PUT',
699
+ body,
700
+ }),
701
+ patchStateMount: (name: string, module: string, body: StateMountBody) =>
702
+ req(
703
+ `/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`,
704
+ s.stateMountUpdated,
705
+ { method: 'PATCH', body },
706
+ ),
707
+ unmountStateModule: (name: string, module: string) =>
708
+ req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateUnmounted, {
709
+ method: 'DELETE',
710
+ }),
711
+ // -- state subjects + records -------------------------------------------
712
+ listStateSubjects: (
713
+ name: string,
714
+ params: { kind?: string } & StatePageQuery,
715
+ signal?: AbortSignal,
716
+ ) =>
717
+ req(`/api/states/${encodeSegment(name)}/subjects`, s.subjectPage, {
718
+ signal,
719
+ query: { kind: params.kind, limit: params.limit, cursor: params.cursor },
720
+ }),
721
+ // The content search body is a single JSONB containment document (`filters`); the
722
+ // server refuses an empty one, so the UI validates a non-empty object first.
723
+ searchStateRecords: (
724
+ name: string,
725
+ body: { filters: Record<string, unknown> } & StatePageQuery,
726
+ signal?: AbortSignal,
727
+ ) =>
728
+ req(`/api/states/${encodeSegment(name)}/records/search`, s.recordSearchPage, {
729
+ method: 'POST',
730
+ body,
731
+ signal,
732
+ }),
733
+ // A subject with no document yet reads as `null` (a 200 `{data: null}`, the record
734
+ // page's Create path) — never an error, so the schema is nullable.
735
+ getStateRecord: (name: string, subject: StateSubjectRef, signal?: AbortSignal) =>
736
+ req(stateRecordPath(name, subject), s.recordView.nullable(), { signal }),
737
+ putStateRecord: (name: string, subject: StateSubjectRef, data: Record<string, unknown>) =>
738
+ req(stateRecordPath(name, subject), s.recordView, { method: 'PUT', body: data }),
739
+ patchStateRecord: (name: string, subject: StateSubjectRef, data: Record<string, unknown>) =>
740
+ req(stateRecordPath(name, subject), s.recordView, { method: 'PATCH', body: data }),
741
+ applyStateRecord: (
742
+ name: string,
743
+ subject: StateSubjectRef,
744
+ ops: readonly Record<string, unknown>[],
745
+ ) =>
746
+ req(`${stateRecordPath(name, subject)}/deltas`, s.applyResult, {
747
+ method: 'POST',
748
+ body: { ops },
749
+ }),
750
+ deleteStateRecord: (name: string, subject: StateSubjectRef) =>
751
+ req(stateRecordPath(name, subject), s.recordErased, { method: 'DELETE' }),
752
+ // `mode` is required server-side (`switch` drops, `merge` combines); the fold returns
753
+ // a report, not the surviving record.
754
+ foldStateRecord: (
755
+ name: string,
756
+ subject: StateSubjectRef,
757
+ into: StateSubjectRef,
758
+ mode: 'switch' | 'merge',
759
+ ) =>
760
+ req(`${stateRecordPath(name, subject)}/fold`, s.stateFoldReport, {
761
+ method: 'POST',
762
+ body: { into, mode },
763
+ }),
764
+ listStateWrites: (
765
+ name: string,
766
+ subject: StateSubjectRef,
767
+ params?: StatePageQuery,
768
+ signal?: AbortSignal,
769
+ ) =>
770
+ req(`${stateRecordPath(name, subject)}/writes`, s.writesPage, {
771
+ signal,
772
+ query: { limit: params?.limit, cursor: params?.cursor },
773
+ }),
774
+ stateConsumers: (name: string, signal?: AbortSignal) =>
775
+ req(`/api/states/${encodeSegment(name)}/consumers`, s.stateConsumers, { signal }),
776
+ // -- state-module documents (top-level sibling collection) ---------------
777
+ listStateModules: (signal?: AbortSignal) =>
778
+ req('/api/state-modules', s.stateModuleList, { signal }),
779
+ getStateModule: (name: string, signal?: AbortSignal) =>
780
+ req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDocument, { signal }),
781
+ putStateModule: (name: string, body: StateModuleBody, replace?: boolean) =>
782
+ req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDocument, {
783
+ method: 'PUT',
784
+ body,
785
+ query: replace === true ? { replace: 'true' } : undefined,
786
+ }),
787
+ deleteStateModule: (name: string) =>
788
+ req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDeleted, { method: 'DELETE' }),
789
+ // -- state retention -----------------------------------------------------
790
+ pruneStateRetention: () =>
791
+ req('/api/state-retention/prune', s.stateRetentionPruned, { method: 'POST', body: {} }),
792
+
609
793
  // -- tool_meta (the organizational overlay) ------------------------------
610
794
  // The folder tree + per-tool overlay rows in one read; the features merge the
611
795
  // rows against the live tool list and native tags client-side.
package/src/errors.ts CHANGED
@@ -9,11 +9,18 @@ export class ApiError extends Error {
9
9
  readonly status: number;
10
10
  /** The optional machine-readable `code` from the `{ error, code }` envelope. */
11
11
  readonly code: string | undefined;
12
- constructor(message: string, status: number, code?: string) {
12
+ /**
13
+ * The response's `Retry-After` delay in seconds, when it carried one (the
14
+ * retriable reloading `503` does). A caller that retries waits this long; a
15
+ * caller that does not ignores it.
16
+ */
17
+ readonly retryAfterSeconds?: number;
18
+ constructor(message: string, status: number, code?: string, retryAfterSeconds?: number) {
13
19
  super(message);
14
20
  this.name = 'ApiError';
15
21
  this.status = status;
16
22
  this.code = code;
23
+ this.retryAfterSeconds = retryAfterSeconds;
17
24
  }
18
25
  }
19
26
 
package/src/http.ts CHANGED
@@ -84,6 +84,14 @@ function buildUrl(base: string, path: string, query?: RequestOptions['query']):
84
84
  * header is `X-Api-Key` (the skeleton's access-control middleware also accepts
85
85
  * `Authorization: Bearer`; we use the dedicated key header).
86
86
  */
87
+ /** The `Retry-After` header as whole seconds; `undefined` when absent or not a
88
+ * delay-seconds value (the HTTP-date form is not used by these routes). */
89
+ function retryAfterSeconds(header: string | null): number | undefined {
90
+ if (header === null) return undefined;
91
+ const seconds = Number(header.trim());
92
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
93
+ }
94
+
87
95
  export async function apiRequest<S extends z.ZodType>(
88
96
  config: ApiConfig,
89
97
  path: string,
@@ -104,19 +112,28 @@ export async function apiRequest<S extends z.ZodType>(
104
112
 
105
113
  if (response.status === 401) throw new ApiUnauthorizedError();
106
114
 
115
+ // `Retry-After` rides on the error so a retrying caller waits the delay the server
116
+ // named (the gated routes' reloading 503 carries one) instead of guessing.
117
+ const retryAfter = retryAfterSeconds(response.headers.get('Retry-After'));
118
+
107
119
  let payload: unknown;
108
120
  try {
109
121
  payload = await response.json();
110
122
  } catch {
111
123
  if (response.ok) throw new ApiSchemaError(path, 'response was not valid JSON');
112
- throw new ApiError(response.statusText || 'request failed', response.status);
124
+ throw new ApiError(
125
+ response.statusText || 'request failed',
126
+ response.status,
127
+ undefined,
128
+ retryAfter,
129
+ );
113
130
  }
114
131
 
115
132
  if (!response.ok) {
116
133
  const { message, code } = extractError(payload);
117
134
  const text = message ?? (response.statusText || 'request failed');
118
135
  if (response.status === 409) throw new ApiConflictError(text);
119
- throw new ApiError(text, response.status, code);
136
+ throw new ApiError(text, response.status, code, retryAfter);
120
137
  }
121
138
 
122
139
  if (!isDataEnvelope(payload)) {
package/src/index.ts CHANGED
@@ -25,6 +25,11 @@ export type {
25
25
  CreatePresetBody,
26
26
  SavePresetVersionBody,
27
27
  ValidatePresetBody,
28
+ StateDeclarationBody,
29
+ StateModuleBody,
30
+ StateMountBody,
31
+ StateSubjectRef,
32
+ StatePageQuery,
28
33
  ToolMetaPatch,
29
34
  StorageUploadBody,
30
35
  ConversationThreadFilters,
@@ -101,6 +106,30 @@ export type {
101
106
  PresetValidation,
102
107
  PresetVersionTags,
103
108
  PresetExtensionElement,
109
+ StateSubject,
110
+ StateDeclaration,
111
+ StateListItem,
112
+ StateMount,
113
+ StateDetail,
114
+ StateStats,
115
+ StateModuleDocument,
116
+ StateModuleListItem,
117
+ RecordView,
118
+ ApplyResult,
119
+ SubjectRow,
120
+ SubjectPage,
121
+ RecordSearchPage,
122
+ CompletedOrigin,
123
+ WriteEntry,
124
+ WritesPage,
125
+ ConsumerLink,
126
+ ConsumerRow,
127
+ StateConsumers,
128
+ StateMounted,
129
+ StateMountUpdated,
130
+ StateUnmounted,
131
+ StateFoldReport,
132
+ StateRetentionPruned,
104
133
  FolderRecord,
105
134
  ToolMetaRecord,
106
135
  ToolMetaOverlay,
@@ -125,6 +154,10 @@ export type {
125
154
  InteractionCancelled,
126
155
  InteractionsPage,
127
156
  InteractionMediaItem,
157
+ FormOption,
158
+ FormData,
159
+ FormPage,
160
+ FormPages,
128
161
  MediaKind,
129
162
  AnswerFormat,
130
163
  Channels,
@@ -164,6 +197,7 @@ export type {
164
197
  Notifications,
165
198
  ChannelTemplate,
166
199
  HookParams,
200
+ HookSubject,
167
201
  HookList,
168
202
  TriggerAuth,
169
203
  SettingsSchema,
package/src/schemas.ts CHANGED
@@ -943,14 +943,54 @@ export const interactionMediaItem = z.object({
943
943
  });
944
944
  export type InteractionMediaItem = z.infer<typeof interactionMediaItem>;
945
945
 
946
+ /**
947
+ * One per-send choice for a `form` property whose schema is a string (or array of
948
+ * strings). `value` is what the answer carries; `label` (absent OR null both parse
949
+ * to no label) is the human text shown in its place. A send may replace a property's
950
+ * `enum` this way for one ask without republishing the form. Applied per property by
951
+ * the form preview (safeParsed), so a malformed entry is a loud notice, never silent.
952
+ */
953
+ export const formOption = z.object({
954
+ value: z.string(),
955
+ label: z.string().nullish(),
956
+ });
957
+ export type FormOption = z.infer<typeof formOption>;
958
+
959
+ /**
960
+ * Per-send `form` data: `values` prefills top-level properties (keyed by property
961
+ * name), `options` supplies per-send choice lists (keyed by property name). Both
962
+ * default to empty. The form preview safeParses this off `format_payload.data`, so a
963
+ * malformed block is a loud notice rather than a whole-frame parse failure.
964
+ */
965
+ export const formData = z.object({
966
+ values: z.record(z.string(), z.unknown()).default({}),
967
+ options: z.record(z.string(), z.array(formOption)).default({}),
968
+ });
969
+ export type FormData = z.infer<typeof formData>;
970
+
971
+ /**
972
+ * One page of a stepped `form`: a `title` and the ordered top-level property names it
973
+ * groups. `format_payload.pages` is the ordered list of these; absent = one page. The
974
+ * form preview safeParses the list, so a malformed page is a loud notice, never silent.
975
+ */
976
+ export const formPage = z.object({
977
+ title: z.string(),
978
+ fields: z.array(z.string()),
979
+ });
980
+ export type FormPage = z.infer<typeof formPage>;
981
+
982
+ /** The `form` pages list (`format_payload.pages`), safeParsed as a whole by the preview. */
983
+ export const formPages = z.array(formPage);
984
+ export type FormPages = z.infer<typeof formPages>;
985
+
946
986
  export const interaction = z.object({
947
987
  interaction_id: z.string(),
948
988
  group_id: z.string(),
949
989
  question: z.string().default(''),
950
990
  answer_format: answerFormat,
951
- // Format-specific payload: select options, form JSON schema, external url. The
952
- // wire sends `null` for formats that carry none (text/confirm); it is
953
- // normalized to `{}` so consumers always see an object.
991
+ // Format-specific payload: select options, form JSON schema (with optional per-send
992
+ // `data` and `pages`), external url. The wire sends `null` for formats that carry
993
+ // none (text/confirm); it is normalized to `{}` so consumers always see an object.
954
994
  format_payload: z
955
995
  .record(z.string(), z.unknown())
956
996
  .nullish()
@@ -1468,6 +1508,20 @@ export const triggerAuth = z.enum([
1468
1508
  ]);
1469
1509
  export type TriggerAuth = z.infer<typeof triggerAuth>;
1470
1510
 
1511
+ /**
1512
+ * The optional state subject a hook fire targets (mirrors
1513
+ * `tai42_contract.hooks.HookSubject`): the conversation-target scope plus the subject
1514
+ * `kind` and a `key_expr` jq evaluated over the event payload at fire. `null` leaves the
1515
+ * fire with no ambient state context.
1516
+ */
1517
+ export const hookSubject = z.object({
1518
+ target_kind: conversationTargetKind,
1519
+ target_name: z.string(),
1520
+ kind: z.string(),
1521
+ key_expr: z.string(),
1522
+ });
1523
+ export type HookSubject = z.infer<typeof hookSubject>;
1524
+
1471
1525
  export const hookParams = z.object({
1472
1526
  name: z.string(),
1473
1527
  topic: z.string(),
@@ -1475,6 +1529,9 @@ export const hookParams = z.object({
1475
1529
  // The api-key `user_id` the fire runs AS.
1476
1530
  execution_key: z.string().min(1),
1477
1531
  tool_kwargs: z.record(z.string(), z.unknown()).default({}),
1532
+ // The optional state subject the fire targets; `null` leaves it with no ambient
1533
+ // state context (a state tool it calls must then carry an explicit subject).
1534
+ subject: hookSubject.nullable().default(null),
1478
1535
  condition: z.string().nullable().default(null),
1479
1536
  condition_id: z.string().nullable().default(null),
1480
1537
  // The `*_kwargs` pair is non-optional in the contract (defaults to {}, never
@@ -2553,3 +2610,276 @@ export type MarketplaceUninstallResult = z.infer<typeof marketplaceUninstallResu
2553
2610
  // Background tool-run schemas live in their own module; re-exported here so the
2554
2611
  // shared fixture-validation contract resolves them by name alongside the rest.
2555
2612
  export { toolRunSubmitResult, toolRunRecord, toolRunList } from './tool-runs';
2613
+
2614
+ // -- states ------------------------------------------------------------------
2615
+ // Shapes mirror tai42_contract.states.models: a declared JSON document, one per
2616
+ // subject. The base document, module fragments, write regimes and effective schema
2617
+ // are permissive JSON records — the auto-form and JsonTree interpret them at runtime;
2618
+ // a drift throws ApiSchemaError. Fields that WP-3's router is finishing concurrently
2619
+ // (`effective_schema`, `regimes`, list/stats derived columns) are modelled optional so
2620
+ // a server that has not yet composed them still parses.
2621
+
2622
+ /** One addressed subject: the conversation-target scope plus the (kind, key) within it. */
2623
+ export const stateSubject = z.object({
2624
+ target_kind: conversationTargetKind,
2625
+ target_name: z.string(),
2626
+ kind: z.string(),
2627
+ key: z.string(),
2628
+ });
2629
+ export type StateSubject = z.infer<typeof stateSubject>;
2630
+
2631
+ /** One absolute write-regime rule composed over the mounts: `{path, regime}`, permissive. */
2632
+ export const stateRegime = z.record(z.string(), z.unknown());
2633
+
2634
+ /**
2635
+ * A declared state served on every read. `schema` is the base JSON Schema;
2636
+ * `effective_schema` (the base composed with every mounted module's fragment) and
2637
+ * `regimes` (the absolute write-regime rules) are platform-COMPUTED and served on every
2638
+ * read — modelled optional so a write body that omits them, or a server yet to compose
2639
+ * them, still parses. A drift throws ApiSchemaError.
2640
+ */
2641
+ export const stateDeclaration = z.object({
2642
+ name: z.string(),
2643
+ description: z.string().default(''),
2644
+ schema: jsonSchema,
2645
+ subject_kinds: z.array(z.string()),
2646
+ default_subject_kind: z.string(),
2647
+ retention_days: z.number().nullable().default(null),
2648
+ effective_schema: jsonSchema.nullable().optional(),
2649
+ regimes: z.array(stateRegime).nullable().optional(),
2650
+ });
2651
+ export type StateDeclaration = z.infer<typeof stateDeclaration>;
2652
+
2653
+ /**
2654
+ * One row of `GET /api/states` — a served declaration (`list_states` dumps every
2655
+ * `StateDeclaration`) plus `updated_at` (the row's last-write timestamp, ISO or
2656
+ * `null`). The list carries no record count; the `Records` column reads
2657
+ * `getStateStats` lazily per row.
2658
+ */
2659
+ export const stateListItem = stateDeclaration.extend({
2660
+ updated_at: z.string().nullable().default(null),
2661
+ });
2662
+ export type StateListItem = z.infer<typeof stateListItem>;
2663
+ export const stateList = z.array(stateListItem);
2664
+
2665
+ /** One module mount on a state: where the fragment lands + its param/declaration values. */
2666
+ export const stateMount = z.object({
2667
+ module: z.string(),
2668
+ path: z.array(z.string()),
2669
+ parameters: z.record(z.string(), z.unknown()).default({}),
2670
+ declarations: z.record(z.string(), z.unknown()).default({}),
2671
+ });
2672
+ export type StateMount = z.infer<typeof stateMount>;
2673
+ export const stateMountList = z.array(stateMount);
2674
+
2675
+ /** `GET /api/states/{name}` — the declaration (with effective schema + regimes) plus mounts. */
2676
+ export const stateDetail = stateDeclaration.extend({
2677
+ mounts: z.array(stateMount).default([]),
2678
+ });
2679
+ export type StateDetail = z.infer<typeof stateDetail>;
2680
+
2681
+ /**
2682
+ * `GET /api/states/{name}/stats` — `records` (total documents), `per_field` (the count
2683
+ * of records carrying each base-schema property), `per_kind` (records per subject kind)
2684
+ * and `consumers` (the count of listable consumers). The Records tab shows `records`;
2685
+ * the states list reads it lazily for its Records column.
2686
+ */
2687
+ export const stateStats = z.object({
2688
+ records: z.number(),
2689
+ per_field: z.record(z.string(), z.number()).default({}),
2690
+ per_kind: z.record(z.string(), z.number()).default({}),
2691
+ consumers: z.number().default(0),
2692
+ });
2693
+ export type StateStats = z.infer<typeof stateStats>;
2694
+
2695
+ /** The platform half of a state-module document (mirrors StateModuleDocument). */
2696
+ export const stateModuleDocument = z.object({
2697
+ kind: z.literal('state-module').default('state-module'),
2698
+ name: z.string(),
2699
+ description: z.string().default(''),
2700
+ parameters: z.record(z.string(), z.unknown()).default({}),
2701
+ schema: jsonSchema,
2702
+ regimes: z.array(stateRegime).default([]),
2703
+ declarations: z.record(z.string(), z.unknown()).nullable().default(null),
2704
+ trace: z.record(z.string(), z.unknown()).default({}),
2705
+ });
2706
+ export type StateModuleDocument = z.infer<typeof stateModuleDocument>;
2707
+
2708
+ /**
2709
+ * One row of `GET /api/state-modules`. The module document plus the two derived columns
2710
+ * the Modules tab shows: `mounted_on` (how many states mount it — a delete is refused
2711
+ * while > 0) and `shipped_default` (a platform-shipped module). ASSUMED derived fields
2712
+ * (WP-3 owns the list router); the document fields are verbatim.
2713
+ */
2714
+ export const stateModuleListItem = stateModuleDocument.extend({
2715
+ mounted_on: z.number().default(0),
2716
+ shipped_default: z.boolean().default(false),
2717
+ });
2718
+ export type StateModuleListItem = z.infer<typeof stateModuleListItem>;
2719
+ export const stateModuleList = z.array(stateModuleListItem);
2720
+
2721
+ /** A read record: the state, its subject, the document + monotonic seq, and any fold. */
2722
+ export const recordView = z.object({
2723
+ state: z.string(),
2724
+ subject: stateSubject,
2725
+ data: z.record(z.string(), z.unknown()).default({}),
2726
+ seq: z.number(),
2727
+ canonical_subject: stateSubject,
2728
+ folded_from: z.array(stateSubject).default([]),
2729
+ });
2730
+ export type RecordView = z.infer<typeof recordView>;
2731
+
2732
+ /** The outcome of an `apply` (a delta batch): whether it applied + the resulting doc. */
2733
+ export const applyResult = z.object({
2734
+ applied: z.boolean(),
2735
+ data: z.record(z.string(), z.unknown()).nullable().default(null),
2736
+ seq: z.number().nullable().default(null),
2737
+ skipped: z.array(z.record(z.string(), z.unknown())).default([]),
2738
+ });
2739
+ export type ApplyResult = z.infer<typeof applyResult>;
2740
+
2741
+ /**
2742
+ * One paged subject row: the addressed `subject` plus its record's last-write
2743
+ * `updated_at` — epoch seconds (the store's `extract(epoch FROM updated_at)`), served
2744
+ * raw (no `mode="json"` dump), and `NOT NULL` on every record, so a plain number.
2745
+ */
2746
+ export const subjectRow = z.object({
2747
+ subject: stateSubject,
2748
+ updated_at: z.number(),
2749
+ });
2750
+ export type SubjectRow = z.infer<typeof subjectRow>;
2751
+
2752
+ /**
2753
+ * `GET /api/states/{name}/subjects` — a keyset page of subject rows (optionally of one
2754
+ * `kind`): the matched `subjects` plus the `next_cursor` a "Load more" carries back
2755
+ * (`null` at the end).
2756
+ */
2757
+ export const subjectPage = z.object({
2758
+ subjects: z.array(subjectRow),
2759
+ next_cursor: z.string().nullable().default(null),
2760
+ });
2761
+ export type SubjectPage = z.infer<typeof subjectPage>;
2762
+
2763
+ /**
2764
+ * `POST /api/states/{name}/records/search` — a keyset page of the subject rows whose
2765
+ * document contains the filters (a JSONB containment match). Each hit's `subject` opens
2766
+ * the record page; `next_cursor` pages the rest.
2767
+ */
2768
+ export const recordSearchPage = z.object({
2769
+ matches: z.array(subjectRow),
2770
+ next_cursor: z.string().nullable().default(null),
2771
+ });
2772
+ export type RecordSearchPage = z.infer<typeof recordSearchPage>;
2773
+
2774
+ /** A WriteEntry's origin completed by the platform chokepoint (mirrors CompletedOrigin). */
2775
+ export const completedOrigin = z.object({
2776
+ consumer: z.string().nullable().default(null),
2777
+ // Opaque consumer-supplied provenance JSON the platform stores as is.
2778
+ meta: z.record(z.string(), z.unknown()).nullable().default(null),
2779
+ run_id: z.string().nullable().default(null),
2780
+ op_id: z.string().nullable().default(null),
2781
+ door: z.string(),
2782
+ actor: z.string().nullable().default(null),
2783
+ turn_id: z.string().nullable().default(null),
2784
+ inbound_id: z.string().nullable().default(null),
2785
+ });
2786
+ export type CompletedOrigin = z.infer<typeof completedOrigin>;
2787
+
2788
+ /** One row of a subject's audit trail (mirrors WriteEntry). `paths` are absolute. */
2789
+ export const writeEntry = z.object({
2790
+ seq: z.number(),
2791
+ at: z.string(),
2792
+ origin: completedOrigin,
2793
+ paths: z.array(z.array(z.union([z.string(), z.number()]))).default([]),
2794
+ });
2795
+ export type WriteEntry = z.infer<typeof writeEntry>;
2796
+
2797
+ /** `GET /api/states/{name}/records/.../writes` — a keyset page of the audit trail. */
2798
+ export const writesPage = z.object({
2799
+ items: z.array(writeEntry),
2800
+ next_cursor: z.string().nullable().default(null),
2801
+ });
2802
+ export type WritesPage = z.infer<typeof writesPage>;
2803
+
2804
+ /** Where the Studio opens a consumer (mirrors ConsumerLink): a token+search OR plugin path. */
2805
+ export const consumerLink = z.object({
2806
+ token: z.string().nullable().default(null),
2807
+ plugin_path: z.string().nullable().default(null),
2808
+ search: z.record(z.string(), z.unknown()).nullable().default(null),
2809
+ });
2810
+ export type ConsumerLink = z.infer<typeof consumerLink>;
2811
+
2812
+ /**
2813
+ * One thing that binds a state (mirrors ConsumerRow): its `kind` (flow / hook / schedule
2814
+ * / agent), `name`, human `detail` and optional `link`. `unavailable` marks a consumer
2815
+ * family that cannot be listed on this deployment (e.g. no scheduling backend), surfaced
2816
+ * as a muted line — never swallowed.
2817
+ */
2818
+ export const consumerRow = z.object({
2819
+ kind: z.string(),
2820
+ name: z.string().nullable().default(null),
2821
+ detail: z.string().nullable().default(null),
2822
+ link: consumerLink.nullable().default(null),
2823
+ unavailable: z.string().nullable().default(null),
2824
+ });
2825
+ export type ConsumerRow = z.infer<typeof consumerRow>;
2826
+
2827
+ /** `GET /api/states/{name}/consumers` — the union of every registered lister's rows. */
2828
+ export const stateConsumers = z.array(consumerRow);
2829
+ export type StateConsumers = z.infer<typeof stateConsumers>;
2830
+
2831
+ /** `DELETE /api/states/{name}` — the removed state. */
2832
+ export const stateDeleted = z.object({ name: z.string(), deleted: z.literal(true) });
2833
+
2834
+ /** `PUT /api/states/{name}/mounts/{module}` — the module now mounted on the state. */
2835
+ export const stateMounted = z.object({
2836
+ mounted: z.literal(true),
2837
+ state: z.string(),
2838
+ module: z.string(),
2839
+ });
2840
+ export type StateMounted = z.infer<typeof stateMounted>;
2841
+
2842
+ /** `PATCH /api/states/{name}/mounts/{module}` — the mount whose declarations were rewritten. */
2843
+ export const stateMountUpdated = z.object({
2844
+ updated: z.literal(true),
2845
+ state: z.string(),
2846
+ module: z.string(),
2847
+ });
2848
+ export type StateMountUpdated = z.infer<typeof stateMountUpdated>;
2849
+
2850
+ /** `DELETE /api/states/{name}/mounts/{module}` — the module unmounted from the state. */
2851
+ export const stateUnmounted = z.object({
2852
+ unmounted: z.literal(true),
2853
+ state: z.string(),
2854
+ module: z.string(),
2855
+ });
2856
+ export type StateUnmounted = z.infer<typeof stateUnmounted>;
2857
+
2858
+ /**
2859
+ * `POST /api/states/{name}/records/.../fold` — the fold report: the `mode`, the
2860
+ * `{kind, key}` of the folded `from` and surviving `into` subjects, whether the fold
2861
+ * was `already` in place, and how many records were `flattened` into the survivor.
2862
+ */
2863
+ export const stateFoldReport = z.object({
2864
+ mode: z.enum(['switch', 'merge']),
2865
+ from: z.object({ kind: z.string(), key: z.string() }),
2866
+ into: z.object({ kind: z.string(), key: z.string() }),
2867
+ already: z.boolean(),
2868
+ flattened: z.number().default(0),
2869
+ });
2870
+ export type StateFoldReport = z.infer<typeof stateFoldReport>;
2871
+
2872
+ /** `DELETE /api/state-modules/{name}` — the removed module document. */
2873
+ export const stateModuleDeleted = z.object({ name: z.string(), deleted: z.literal(true) });
2874
+
2875
+ /** `DELETE /api/states/{name}/records/...` — the erased record marker. */
2876
+ export const recordErased = z.object({ erased: z.literal(true) });
2877
+
2878
+ /**
2879
+ * `POST /api/state-retention/prune` — the retention sweep's per-state deleted counts
2880
+ * (`{}` when nothing expired); the UI sums the values for its total.
2881
+ */
2882
+ export const stateRetentionPruned = z.object({
2883
+ pruned: z.record(z.string(), z.number()).default({}),
2884
+ });
2885
+ export type StateRetentionPruned = z.infer<typeof stateRetentionPruned>;