@tai42/api-client 13.3.0 → 15.0.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.
package/src/client.ts CHANGED
@@ -65,9 +65,9 @@ export interface ApiKeyBody {
65
65
  // `null` is an explicit clear on edit (PATCH-style PUT): absent preserves the
66
66
  // stored policy data, `null` wipes it. Create never sends `null`.
67
67
  readonly policy_data?: Record<string, unknown> | null;
68
- readonly condition?: string | null;
69
- readonly condition_id?: string | null;
70
- readonly condition_kwargs?: Record<string, unknown> | null;
68
+ // The access-control condition: an authored templated-text value (inline content
69
+ // or a stored template id), or `null` to explicitly clear it on edit.
70
+ readonly condition?: s.TemplatedText | null;
71
71
  }
72
72
 
73
73
  /**
@@ -82,16 +82,13 @@ export interface ClaimLinkBody {
82
82
  }
83
83
 
84
84
  /**
85
- * Body for the fail-closed jq guard (POST `/api/auth/validate-condition`).
86
- * `condition` (inline jq) and `condition_id` (named template) are mutually
87
- * exclusive — send exactly one. `sample_context` is a `JqAuthContext`-shaped
88
- * sample; when present the guard also evaluates the condition against it and
89
- * returns the boolean `result` (else `result` is `null`).
85
+ * Body for the fail-closed jq guard (POST `/api/auth/validate-condition`). The guard
86
+ * compile-checks the inline jq `condition` the author typed. `sample_context` is a
87
+ * `JqAuthContext`-shaped sample; when present the guard also evaluates the condition
88
+ * against it and returns the boolean `result` (else `result` is `null`).
90
89
  */
91
90
  export interface ValidateConditionBody {
92
91
  readonly condition?: string;
93
- readonly condition_id?: string;
94
- readonly condition_kwargs?: Record<string, unknown>;
95
92
  readonly sample_context?: Record<string, unknown>;
96
93
  }
97
94
 
@@ -197,6 +194,8 @@ export interface CreatePresetBody {
197
194
  readonly extensions?: readonly s.PresetExtensionElement[][];
198
195
  readonly output_schema?: Record<string, unknown> | null;
199
196
  readonly input_schema?: Record<string, unknown> | null;
197
+ // The optional door-layer state binding applied around every run of this preset.
198
+ readonly state_binding?: s.StateBinding | null;
200
199
  }
201
200
 
202
201
  /**
@@ -205,10 +204,11 @@ export interface CreatePresetBody {
205
204
  * sentinel is uniform: omitted carries the active version's value forward; an
206
205
  * explicit `[]` clears `extensions`, and an explicit `null` clears `output_schema`
207
206
  * or `input_schema`. `input_schema` is the optional author-set input JSON Schema and
208
- * follows the same carry-forward sentinel as `output_schema`. `description` carries
209
- * forward when omitted; an explicit non-empty string sets it (the API rejects an
210
- * explicit empty one). Categorization tags are not a version field — they live in the
211
- * tool_meta overlay.
207
+ * follows the same carry-forward sentinel as `output_schema`. `state_binding` is the
208
+ * optional door-layer binding and follows the same sentinel: omitted carries the active
209
+ * one forward, an explicit `null` clears it. `description` carries forward when omitted;
210
+ * an explicit non-empty string sets it (the API rejects an explicit empty one).
211
+ * Categorization tags are not a version field — they live in the tool_meta overlay.
212
212
  */
213
213
  export interface SavePresetVersionBody {
214
214
  readonly fixed_kwargs?: Record<string, unknown>;
@@ -216,6 +216,7 @@ export interface SavePresetVersionBody {
216
216
  readonly extensions?: readonly s.PresetExtensionElement[][];
217
217
  readonly output_schema?: Record<string, unknown> | null;
218
218
  readonly input_schema?: Record<string, unknown> | null;
219
+ readonly state_binding?: s.StateBinding | null;
219
220
  }
220
221
 
221
222
  /**
@@ -233,6 +234,10 @@ export interface ValidatePresetBody {
233
234
  readonly extensions?: readonly s.PresetExtensionElement[][];
234
235
  readonly output_schema?: Record<string, unknown> | null;
235
236
  readonly input_schema?: Record<string, unknown> | null;
237
+ // The optional door-layer state binding to dry-run — validated (never mounted) by the
238
+ // same checks the write would run. In VERSION mode it follows the save-version sentinel:
239
+ // omitted carries the active binding forward, an explicit `null` clears it.
240
+ readonly state_binding?: s.StateBinding | null;
236
241
  }
237
242
 
238
243
  /**
@@ -520,8 +525,8 @@ export interface StateDeclarationBody {
520
525
  readonly retention_days?: number | null;
521
526
  }
522
527
 
523
- /** A state-module document write body (`PUT /api/state-modules/{name}`, the platform half). */
524
- export interface StateModuleBody {
528
+ /** A state-template document write body (`PUT /api/state-templates/{name}`, the platform half). */
529
+ export interface StateTemplateBody {
525
530
  readonly name: string;
526
531
  readonly description?: string;
527
532
  readonly parameters?: Record<string, unknown>;
@@ -531,11 +536,15 @@ export interface StateModuleBody {
531
536
  readonly trace?: Record<string, unknown>;
532
537
  }
533
538
 
534
- /** A module mount / re-mount body (`PUT|PATCH /api/states/{name}/mounts/{module}`). */
535
- export interface StateMountBody {
539
+ /** A template attach / re-attach body (`PUT|PATCH /api/states/{name}/attachments/{template}`). */
540
+ export interface StateAttachmentBody {
536
541
  readonly path?: string[];
537
542
  readonly parameters?: Record<string, unknown>;
538
543
  readonly declarations?: Record<string, unknown>;
544
+ // Reconcile directive for a re-attach that would orphan open records: retry with
545
+ // `{ orphans: "close", resolution: "<name>" }` to close them (the server refuses
546
+ // without it). Omitted on a first attach.
547
+ readonly options?: Record<string, unknown>;
539
548
  }
540
549
 
541
550
  /** The four-part subject a record route addresses in its path. */
@@ -565,6 +574,22 @@ function stateRecordPath(name: string, subject: StateSubjectRef): string {
565
574
  );
566
575
  }
567
576
 
577
+ /**
578
+ * Encode a template jq's declared params as JSON query values: the record `template-jq`
579
+ * route reads each `?<param>=<json>` as a JSON-decoded argument, so a string, number,
580
+ * array or object round-trips faithfully. An `undefined` value is dropped (an
581
+ * omitted optional param).
582
+ */
583
+ function encodeJqParams(params?: Record<string, unknown>): Record<string, string> | undefined {
584
+ if (params === undefined) return undefined;
585
+ const query: Record<string, string> = {};
586
+ for (const [key, value] of Object.entries(params)) {
587
+ if (value === undefined) continue;
588
+ query[key] = JSON.stringify(value);
589
+ }
590
+ return query;
591
+ }
592
+
568
593
  export function createApiClient(config: ApiConfig) {
569
594
  const req = <S extends Parameters<typeof apiRequest>[2]>(
570
595
  path: string,
@@ -676,7 +701,7 @@ export function createApiClient(config: ApiConfig) {
676
701
  // -- states --------------------------------------------------------------
677
702
  // A state is a declared JSON document, one per subject. The list is the
678
703
  // management population; a single state read composes the declaration with its
679
- // effective schema, regimes and mounts. Every record route addresses its subject
704
+ // effective schema, regimes and attachments. Every record route addresses its subject
680
705
  // as four percent-encoded path segments (see `stateRecordPath`).
681
706
  listStates: (signal?: AbortSignal) => req('/api/states', s.stateList, { signal }),
682
707
  getState: (name: string, signal?: AbortSignal) =>
@@ -693,28 +718,33 @@ export function createApiClient(config: ApiConfig) {
693
718
  req(`/api/states/${encodeSegment(name)}`, s.stateDeleted, { method: 'DELETE' }),
694
719
  getStateStats: (name: string, signal?: AbortSignal) =>
695
720
  req(`/api/states/${encodeSegment(name)}/stats`, s.stateStats, { signal }),
696
- // -- state mounts --------------------------------------------------------
697
- listStateMounts: (name: string, signal?: AbortSignal) =>
698
- req(`/api/states/${encodeSegment(name)}/mounts`, s.stateMountList, { signal }),
699
- getStateMount: (name: string, module: string, signal?: AbortSignal) =>
700
- req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateMount, {
701
- signal,
702
- }),
703
- mountStateModule: (name: string, module: string, body: StateMountBody) =>
704
- req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateMounted, {
705
- method: 'PUT',
706
- body,
707
- }),
708
- patchStateMount: (name: string, module: string, body: StateMountBody) =>
721
+ // -- state template attachments ------------------------------------------
722
+ listStateAttachments: (name: string, signal?: AbortSignal) =>
723
+ req(`/api/states/${encodeSegment(name)}/attachments`, s.stateAttachmentList, { signal }),
724
+ getStateAttachment: (name: string, template: string, signal?: AbortSignal) =>
725
+ req(
726
+ `/api/states/${encodeSegment(name)}/attachments/${encodeSegment(template)}`,
727
+ s.stateAttachment,
728
+ { signal },
729
+ ),
730
+ attachStateTemplate: (name: string, template: string, body: StateAttachmentBody) =>
709
731
  req(
710
- `/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`,
711
- s.stateMountUpdated,
732
+ `/api/states/${encodeSegment(name)}/attachments/${encodeSegment(template)}`,
733
+ s.stateAttached,
734
+ { method: 'PUT', body },
735
+ ),
736
+ patchStateAttachment: (name: string, template: string, body: StateAttachmentBody) =>
737
+ req(
738
+ `/api/states/${encodeSegment(name)}/attachments/${encodeSegment(template)}`,
739
+ s.stateAttachmentUpdated,
712
740
  { method: 'PATCH', body },
713
741
  ),
714
- unmountStateModule: (name: string, module: string) =>
715
- req(`/api/states/${encodeSegment(name)}/mounts/${encodeSegment(module)}`, s.stateUnmounted, {
716
- method: 'DELETE',
717
- }),
742
+ detachStateTemplate: (name: string, template: string) =>
743
+ req(
744
+ `/api/states/${encodeSegment(name)}/attachments/${encodeSegment(template)}`,
745
+ s.stateDetached,
746
+ { method: 'DELETE' },
747
+ ),
718
748
  // -- state subjects + records -------------------------------------------
719
749
  listStateSubjects: (
720
750
  name: string,
@@ -740,11 +770,11 @@ export function createApiClient(config: ApiConfig) {
740
770
  // A subject with no document yet reads as `null` (a 200 `{data: null}`, the record
741
771
  // page's Create path) — never an error, so the schema is nullable.
742
772
  getStateRecord: (name: string, subject: StateSubjectRef, signal?: AbortSignal) =>
743
- req(stateRecordPath(name, subject), s.recordView.nullable(), { signal }),
773
+ req(stateRecordPath(name, subject), s.stateRecord.nullable(), { signal }),
744
774
  putStateRecord: (name: string, subject: StateSubjectRef, data: Record<string, unknown>) =>
745
- req(stateRecordPath(name, subject), s.recordView, { method: 'PUT', body: data }),
775
+ req(stateRecordPath(name, subject), s.stateRecord, { method: 'PUT', body: data }),
746
776
  patchStateRecord: (name: string, subject: StateSubjectRef, data: Record<string, unknown>) =>
747
- req(stateRecordPath(name, subject), s.recordView, { method: 'PATCH', body: data }),
777
+ req(stateRecordPath(name, subject), s.stateRecord, { method: 'PATCH', body: data }),
748
778
  applyStateRecord: (
749
779
  name: string,
750
780
  subject: StateSubjectRef,
@@ -754,6 +784,35 @@ export function createApiClient(config: ApiConfig) {
754
784
  method: 'POST',
755
785
  body: { ops },
756
786
  }),
787
+ // Evaluate a template jq over the subject: each declared param is JSON-encoded into
788
+ // the query string (`?<param>=<json>`); the response is `{name, purpose, value}`. An
789
+ // `attachment.name` jq is percent-encoded as one path segment.
790
+ evalTemplateJq: (
791
+ name: string,
792
+ subject: StateSubjectRef,
793
+ jqName: string,
794
+ params?: Record<string, unknown>,
795
+ signal?: AbortSignal,
796
+ ) =>
797
+ req(
798
+ `${stateRecordPath(name, subject)}/template-jq/${encodeSegment(jqName)}`,
799
+ s.templateJqResult,
800
+ { signal, query: encodeJqParams(params) },
801
+ ),
802
+ // Apply an `update`-purpose template jq to the subject: `input` is the jq's argument,
803
+ // `op_id` its idempotency key (a replay is a no-op). The jq lands its op batch through
804
+ // the apply chokepoint. An `attachment.name` jq is percent-encoded as one path segment.
805
+ applyTemplateJq: (
806
+ name: string,
807
+ subject: StateSubjectRef,
808
+ jqName: string,
809
+ body?: { input?: unknown; op_id?: string },
810
+ ) =>
811
+ req(
812
+ `${stateRecordPath(name, subject)}/template-jq/${encodeSegment(jqName)}`,
813
+ s.templateJqApplyResult,
814
+ { method: 'POST', body: body ?? {} },
815
+ ),
757
816
  deleteStateRecord: (name: string, subject: StateSubjectRef) =>
758
817
  req(stateRecordPath(name, subject), s.recordErased, { method: 'DELETE' }),
759
818
  // `mode` is required server-side (`switch` drops, `merge` combines); the fold returns
@@ -780,19 +839,21 @@ export function createApiClient(config: ApiConfig) {
780
839
  }),
781
840
  stateConsumers: (name: string, signal?: AbortSignal) =>
782
841
  req(`/api/states/${encodeSegment(name)}/consumers`, s.stateConsumers, { signal }),
783
- // -- state-module documents (top-level sibling collection) ---------------
784
- listStateModules: (signal?: AbortSignal) =>
785
- req('/api/state-modules', s.stateModuleList, { signal }),
786
- getStateModule: (name: string, signal?: AbortSignal) =>
787
- req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDocument, { signal }),
788
- putStateModule: (name: string, body: StateModuleBody, replace?: boolean) =>
789
- req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDocument, {
842
+ // -- state-template documents (top-level sibling collection) ---------------
843
+ listStateTemplates: (signal?: AbortSignal) =>
844
+ req('/api/state-templates', s.stateTemplateList, { signal }),
845
+ getStateTemplate: (name: string, signal?: AbortSignal) =>
846
+ req(`/api/state-templates/${encodeSegment(name)}`, s.stateTemplateDocument, { signal }),
847
+ putStateTemplate: (name: string, body: StateTemplateBody, replace?: boolean) =>
848
+ req(`/api/state-templates/${encodeSegment(name)}`, s.stateTemplateDocument, {
790
849
  method: 'PUT',
791
850
  body,
792
851
  query: replace === true ? { replace: 'true' } : undefined,
793
852
  }),
794
- deleteStateModule: (name: string) =>
795
- req(`/api/state-modules/${encodeSegment(name)}`, s.stateModuleDeleted, { method: 'DELETE' }),
853
+ deleteStateTemplate: (name: string) =>
854
+ req(`/api/state-templates/${encodeSegment(name)}`, s.stateTemplateDeleted, {
855
+ method: 'DELETE',
856
+ }),
796
857
  // -- state retention -----------------------------------------------------
797
858
  pruneStateRetention: () =>
798
859
  req('/api/state-retention/prune', s.stateRetentionPruned, { method: 'POST', body: {} }),
@@ -902,11 +963,11 @@ export function createApiClient(config: ApiConfig) {
902
963
  // root; those surface as loud errors, never a faked success.
903
964
  deleteTemplateDir: (path: string) =>
904
965
  req('/api/delete-template-dir', s.templateDirDeleted, { method: 'POST', body: { path } }),
905
- renderTemplate: (body: {
906
- content?: string;
907
- template_id?: string;
908
- kwargs?: Record<string, unknown>;
909
- }) => req('/api/render-template', s.templateRendered, { method: 'POST', body }),
966
+ // Render one authored text — inline `content` OR a stored template `id`, plus its
967
+ // render `kwargs` — and parse the rendered output. The value is the shared
968
+ // `TemplatedText` (exactly one source); the route wraps it under `text`.
969
+ renderTemplate: (text: s.TemplatedText) =>
970
+ req('/api/render-template', s.templateRendered, { method: 'POST', body: { text } }),
910
971
  clearTemplatesCache: () =>
911
972
  req('/api/clear-templates-cache', s.templateCacheCleared, { method: 'POST' }),
912
973
 
@@ -1555,6 +1616,8 @@ export function createApiClient(config: ApiConfig) {
1555
1616
  tool_name: string;
1556
1617
  tool_kwargs: Record<string, unknown>;
1557
1618
  schedule_kwargs: Record<string, unknown>;
1619
+ // The optional door-layer state binding applied around every fire of this schedule.
1620
+ state_binding?: s.StateBinding | null;
1558
1621
  }) => req('/api/schedules', s.jsonValue, { method: 'POST', body }),
1559
1622
  deleteSchedule: (name: string) =>
1560
1623
  req(`/api/schedules/${encodeSegment(name)}`, s.jsonValue, { method: 'DELETE' }),
package/src/errors.ts CHANGED
@@ -15,12 +15,26 @@ export class ApiError extends Error {
15
15
  * caller that does not ignores it.
16
16
  */
17
17
  readonly retryAfterSeconds?: number;
18
- constructor(message: string, status: number, code?: string, retryAfterSeconds?: number) {
18
+ /**
19
+ * The full parsed error body (`{ error, code, …extra }`), when the failure carried a
20
+ * JSON body. Operations spread structured follow-up data beside the message (e.g. a
21
+ * reconcile refusal's `{ reconcile: true, orphans: [...] }`), so a UI keys its
22
+ * follow-up on this DATA, not on the prose. `undefined` when the body was not JSON.
23
+ */
24
+ readonly body?: unknown;
25
+ constructor(
26
+ message: string,
27
+ status: number,
28
+ code?: string,
29
+ retryAfterSeconds?: number,
30
+ body?: unknown,
31
+ ) {
19
32
  super(message);
20
33
  this.name = 'ApiError';
21
34
  this.status = status;
22
35
  this.code = code;
23
36
  this.retryAfterSeconds = retryAfterSeconds;
37
+ this.body = body;
24
38
  }
25
39
  }
26
40
 
@@ -34,8 +48,8 @@ export class ApiUnauthorizedError extends ApiError {
34
48
 
35
49
  /** A duplicate answer / alias collision etc. (HTTP 409). */
36
50
  export class ApiConflictError extends ApiError {
37
- constructor(message: string) {
38
- super(message, 409);
51
+ constructor(message: string, body?: unknown) {
52
+ super(message, 409, undefined, undefined, body);
39
53
  this.name = 'ApiConflictError';
40
54
  }
41
55
  }
package/src/http.ts CHANGED
@@ -132,8 +132,10 @@ export async function apiRequest<S extends z.ZodType>(
132
132
  if (!response.ok) {
133
133
  const { message, code } = extractError(payload);
134
134
  const text = message ?? (response.statusText || 'request failed');
135
- if (response.status === 409) throw new ApiConflictError(text);
136
- throw new ApiError(text, response.status, code, retryAfter);
135
+ // The parsed body rides the error so a caller can key a follow-up on its structured
136
+ // fields (e.g. a reconcile refusal's `orphans`), never on the message prose.
137
+ if (response.status === 409) throw new ApiConflictError(text, payload);
138
+ throw new ApiError(text, response.status, code, retryAfter, payload);
137
139
  }
138
140
 
139
141
  if (!isDataEnvelope(payload)) {
package/src/index.ts CHANGED
@@ -26,8 +26,8 @@ export type {
26
26
  SavePresetVersionBody,
27
27
  ValidatePresetBody,
28
28
  StateDeclarationBody,
29
- StateModuleBody,
30
- StateMountBody,
29
+ StateTemplateBody,
30
+ StateAttachmentBody,
31
31
  StateSubjectRef,
32
32
  StatePageQuery,
33
33
  ToolMetaPatch,
@@ -86,6 +86,7 @@ export type {
86
86
  } from './fleet-report';
87
87
  export * as schemas from './schemas';
88
88
  export type {
89
+ TemplatedText,
89
90
  ToolSchema,
90
91
  ToolMediaResult,
91
92
  ToolTagEntry,
@@ -109,13 +110,22 @@ export type {
109
110
  StateSubject,
110
111
  StateDeclaration,
111
112
  StateListItem,
112
- StateMount,
113
+ StateAttachment,
113
114
  StateDetail,
114
115
  StateStats,
115
- StateModuleDocument,
116
- StateModuleListItem,
117
- RecordView,
116
+ TemplateJq,
117
+ TemplateReconcile,
118
+ TemplateDeclarations,
119
+ StateTemplateDocument,
120
+ StateTemplateListItem,
121
+ StateInjection,
122
+ StateUpdate,
123
+ StateAttach,
124
+ StateBinding,
125
+ StateRecord,
118
126
  ApplyResult,
127
+ TemplateJqResult,
128
+ TemplateJqApplyResult,
119
129
  SubjectRow,
120
130
  SubjectPage,
121
131
  RecordSearchPage,
@@ -125,9 +135,9 @@ export type {
125
135
  ConsumerLink,
126
136
  ConsumerRow,
127
137
  StateConsumers,
128
- StateMounted,
129
- StateMountUpdated,
130
- StateUnmounted,
138
+ StateAttached,
139
+ StateAttachmentUpdated,
140
+ StateDetached,
131
141
  StateFoldReport,
132
142
  StateRetentionPruned,
133
143
  FolderRecord,