@agent-native/core 0.154.4 → 0.154.5

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 (34) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  3. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  4. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  5. package/corpus/templates/content/actions/_database-utils.ts +44 -0
  6. package/corpus/templates/content/actions/_property-utils.ts +4 -1
  7. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  8. package/corpus/templates/content/actions/configure-document-property.ts +165 -1
  9. package/corpus/templates/content/actions/delete-document-property.ts +18 -0
  10. package/corpus/templates/content/actions/set-document-property.ts +78 -1
  11. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  12. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  13. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  14. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  15. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  16. package/corpus/templates/content/parity/matrix.md +1 -1
  17. package/corpus/templates/content/parity/matrix.ts +2 -0
  18. package/corpus/templates/content/server/db/schema.ts +39 -3
  19. package/corpus/templates/content/server/plugins/db.ts +30 -0
  20. package/corpus/templates/content/shared/api.ts +75 -2
  21. package/dist/action.d.ts +22 -0
  22. package/dist/action.js +27 -0
  23. package/dist/collab/awareness.d.ts +2 -2
  24. package/dist/collab/routes.d.ts +1 -1
  25. package/dist/index.browser.d.ts +1 -1
  26. package/dist/index.browser.js +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/notifications/routes.d.ts +1 -1
  30. package/dist/observability/routes.d.ts +6 -6
  31. package/dist/secrets/routes.d.ts +3 -3
  32. package/dist/server/action-routes.js +10 -2
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/package.json +1 -1
@@ -1631,6 +1631,11 @@ function DatabaseTable({
1631
1631
  ) {
1632
1632
  if (!databaseId) return null;
1633
1633
  if (isWorkspaceCatalog) return null;
1634
+ const mutationContract = data?.mutationContract;
1635
+ if (!mutationContract) {
1636
+ toast.error(dbText("failedToCreateRow"));
1637
+ return null;
1638
+ }
1634
1639
  const propertyValues = {
1635
1640
  ...databasePropertyValuesForNewItem(filters, properties, filterMode),
1636
1641
  ...propertyValueOverrides,
@@ -1638,8 +1643,10 @@ function DatabaseTable({
1638
1643
  let response;
1639
1644
  try {
1640
1645
  response = await addItem.mutateAsync({
1641
- databaseId,
1642
- title,
1646
+ target: mutationContract.target,
1647
+ expectedSchemaRevision: mutationContract.schemaRevision,
1648
+ idempotencyKey: crypto.randomUUID(),
1649
+ ...(title.trim() ? { title } : {}),
1643
1650
  propertyValues:
1644
1651
  Object.keys(propertyValues).length > 0 ? propertyValues : undefined,
1645
1652
  });
@@ -1650,12 +1657,7 @@ function DatabaseTable({
1650
1657
  });
1651
1658
  return null;
1652
1659
  }
1653
- const createdItem = databaseCreatedItemForImmediatePreview(response, {
1654
- databaseId,
1655
- parentDocument: document,
1656
- title,
1657
- propertyValues,
1658
- });
1660
+ const createdItem = response.createdItem ?? null;
1659
1661
  const needsPreview =
1660
1662
  !!createdItem &&
1661
1663
  databaseCreatedItemNeedsPreview(items, createdItem, options);
@@ -12,6 +12,7 @@ import type {
12
12
  CancelPreparedBuilderSourceUpdateResponse,
13
13
  ChangeContentDatabaseSourceRoleRequest,
14
14
  ContentDatabaseResponse,
15
+ ContentDatabaseRowMutationResult,
15
16
  ContentDatabaseSourceAttachmentAck,
16
17
  ContentDatabaseSourceAttachmentResult,
17
18
  ContentDatabaseItemsPageResponse,
@@ -819,40 +820,40 @@ export function useTrashedContentDatabases() {
819
820
 
820
821
  export function useAddDatabaseItem(documentId: string) {
821
822
  const queryClient = useQueryClient();
822
- return useActionMutation<ContentDatabaseResponse, AddDatabaseItemRequest>(
823
- "add-database-item",
824
- {
825
- skipActionQueryInvalidation: true,
826
- onSuccess: (data) => {
827
- if (data.createdItem) {
828
- queryClient.setQueriesData<ContentDatabaseResponse>(
829
- {
830
- queryKey: ["action", "get-content-database"],
831
- predicate: (query) => {
832
- if (
833
- !isContentDatabaseQueryForDocument(query.queryKey, documentId)
834
- ) {
835
- return false;
836
- }
837
- const params = query.queryKey[2] as {
838
- tableQuery?: unknown;
839
- };
840
- return params.tableQuery === undefined;
841
- },
823
+ return useActionMutation<
824
+ ContentDatabaseRowMutationResult,
825
+ AddDatabaseItemRequest
826
+ >("add-database-item", {
827
+ skipActionQueryInvalidation: true,
828
+ onSuccess: (data) => {
829
+ if (data.createdItem) {
830
+ queryClient.setQueriesData<ContentDatabaseResponse>(
831
+ {
832
+ queryKey: ["action", "get-content-database"],
833
+ predicate: (query) => {
834
+ if (
835
+ !isContentDatabaseQueryForDocument(query.queryKey, documentId)
836
+ ) {
837
+ return false;
838
+ }
839
+ const params = query.queryKey[2] as {
840
+ tableQuery?: unknown;
841
+ };
842
+ return params.tableQuery === undefined;
842
843
  },
843
- (current) =>
844
- applyOptimisticItemToContentDatabase(current, data.createdItem!),
845
- );
846
- }
847
- queryClient.invalidateQueries({
848
- queryKey: contentDatabaseQueryKey(documentId),
849
- });
850
- queryClient.invalidateQueries({
851
- queryKey: ["action", "list-documents"],
852
- });
853
- },
844
+ },
845
+ (current) =>
846
+ applyOptimisticItemToContentDatabase(current, data.createdItem!),
847
+ );
848
+ }
849
+ queryClient.invalidateQueries({
850
+ queryKey: contentDatabaseQueryKey(documentId),
851
+ });
852
+ queryClient.invalidateQueries({
853
+ queryKey: ["action", "list-documents"],
854
+ });
854
855
  },
855
- );
856
+ });
856
857
  }
857
858
 
858
859
  export function useSubmitContentDatabaseForm(documentId: string) {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-10
4
+ ---
5
+
6
+ Database row actions now validate exact schemas and safe retries before changing data.
@@ -9,7 +9,7 @@ This generated matrix tracks whether high-value Content UI operations use the sa
9
9
  | database.lifecycle-and-trash | database | Create, soft-delete, restore, list, and inspect content databases | action-backed | `create-content-database`, `create-inline-content-database`, `delete-content-database`, `get-content-database`, `list-content-databases`, `list-trashed-content-databases`, `restore-content-database` | `app/components/editor/SlashCommandMenu.tsx`, `app/hooks/use-content-database.ts`, `app/hooks/use-documents.ts` | Database pages and database records are created, read, soft-deleted, restored, and listed. | - | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts` | `database-source-scope` | - |
10
10
  | database.private-preview-drafts | database | Persist and reconcile a user's private database-page preview draft | action-backed | `get-preview-document-draft`, `update-preview-document-draft` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-documents.ts` | A user's private preview draft is read, saved, conflict-checked, or deleted without changing the shared database page until the normal save flow applies it. | These per-user editor-state actions are intentionally hidden from agent tools because preview drafts are a private UI recovery mechanism. | - | P1 | covered | `actions/preview-document-draft.db.test.ts` | - | - |
11
11
  | database.properties-and-view-config | database | Configure properties, values, ordering, and saved views | action-backed | `configure-document-property`, `delete-document-property`, `duplicate-document-property`, `get-content-database-personal-view`, `list-document-properties`, `reorder-document-property`, `set-document-property`, `update-content-database-personal-view`, `update-content-database-view` | `app/components/editor/DocumentProperties.tsx`, `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-document-properties.ts` | Property schemas, property values, property order, filters, sorts, grouping, hidden columns, view type, and view settings are stored. | - | - | P0 | covered | `actions/bind-content-database-source-field.db.test.ts`, `actions/content-database-source-actions.test.ts`, `actions/resync-content-database-source.db.test.ts` | `database-source-scope` | - |
12
- | database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
12
+ | database.rows | database | Add, duplicate, move, open, edit, and remove database rows | action-backed | `add-database-item`, `update-database-item`, `upsert-database-item-by-key`, `remove-database-items`, `duplicate-database-items`, `duplicate-database-item`, `migrate-content-database-rows`, `move-database-item`, `set-document-property` | `app/components/editor/DocumentDatabase.tsx`, `app/components/editor/database/DatabaseView.tsx` | Database row memberships and ordering are created, duplicated, moved, edited, and removed without deleting the backing page; bounded migrations atomically update row bodies and properties through the same canonical data model. | - | - | P0 | covered | `actions/database-row-batch-actions.db.test.ts`, `actions/upsert-database-item-by-key.db.test.ts`, `actions/migrate-content-database-rows.db.test.ts`, `parity/__tests__/database-row-batch-reliability.test.ts` | `database-bulk-row-reliability` | - |
13
13
  | database.table-query-page | database | Query one constrained page while retaining database metadata | action-backed | `query-content-database-items` | `app/components/editor/database/DatabaseView.tsx`, `app/hooks/use-content-database.ts` | - | This UI-only bounded projection is intentionally hidden with agentTool: false; agents use get-content-database for the complete database contract. | - | P0 | covered | `actions/content-database-lifecycle.db.test.ts`, `app/hooks/use-content-database.test.ts` | - | - |
14
14
  | editor.agent-assist-prompts | editor | Ask AI from slash generation or comment context | client-assist | - | `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/CommentsSidebar.tsx` | No direct durable mutation; the prompt asks the agent to use document actions when it decides to write. | - | - | P1 | none | - | - | - |
15
15
  | editor.client-formatting-and-insertions | editor | Rich text formatting, selection state, slash block insertion, and copy actions | client-only-ephemeral | - | `app/components/editor/BubbleToolbar.tsx`, `app/components/editor/SlashCommandMenu.tsx`, `app/components/editor/DocumentToolbar.tsx` | - | - | - | P1 | none | - | - | - |
@@ -345,6 +345,7 @@ export const parityMatrix: ParityRow[] = [
345
345
  status: "action-backed",
346
346
  actions: [
347
347
  "add-database-item",
348
+ "update-database-item",
348
349
  "upsert-database-item-by-key",
349
350
  "remove-database-items",
350
351
  "duplicate-database-items",
@@ -360,6 +361,7 @@ export const parityMatrix: ParityRow[] = [
360
361
  followUpPR: null,
361
362
  coverageRefs: [
362
363
  "actions/database-row-batch-actions.db.test.ts",
364
+ "actions/upsert-database-item-by-key.db.test.ts",
363
365
  "actions/migrate-content-database-rows.db.test.ts",
364
366
  "parity/__tests__/database-row-batch-reliability.test.ts",
365
367
  ],
@@ -234,6 +234,7 @@ export const contentDatabases = table(
234
234
  ownerBlockId: text("owner_block_id"),
235
235
  title: text("title").notNull().default("Untitled database"),
236
236
  systemRole: text("system_role"),
237
+ naturalKeyPropertyId: text("natural_key_property_id"),
237
238
  viewConfigJson: text("view_config_json").notNull().default("{}"),
238
239
  filesSystemPropertiesSeeded: integer("files_system_properties_seeded")
239
240
  .notNull()
@@ -287,9 +288,9 @@ export const contentDatabaseItems = table(
287
288
  ],
288
289
  );
289
290
 
290
- // Opt-in stable-key claims are the durable concurrency fence for the generic
291
- // database-row upsert action. They intentionally do not constrain ordinary
292
- // property editing or change add-database-item behavior.
291
+ // Opt-in stable-key claims are the durable concurrency fence for configured
292
+ // natural-key upserts. Ordinary property editing stays independent until a
293
+ // text property is explicitly selected as the database's natural key.
293
294
  export const contentDatabaseItemKeyClaims = table(
294
295
  "content_database_item_key_claims",
295
296
  {
@@ -494,6 +495,41 @@ export const contentDatabaseMigrationReceipts = table(
494
495
  ],
495
496
  );
496
497
 
498
+ export const contentDatabaseRowMutationReceipts = table(
499
+ "content_database_row_mutation_receipts",
500
+ {
501
+ id: text("id").primaryKey(),
502
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
503
+ orgId: text("org_id"),
504
+ spaceId: text("space_id").notNull(),
505
+ databaseId: text("database_id").notNull(),
506
+ databaseDocumentId: text("database_document_id").notNull(),
507
+ operation: text("operation").notNull(),
508
+ itemId: text("item_id").notNull(),
509
+ documentId: text("document_id").notNull(),
510
+ idempotencyKey: text("idempotency_key").notNull(),
511
+ payloadDigest: text("payload_digest").notNull(),
512
+ schemaRevision: text("schema_revision").notNull(),
513
+ preRowRevision: text("pre_row_revision"),
514
+ postRowRevision: text("post_row_revision").notNull(),
515
+ resultJson: text("result_json").notNull().default("{}"),
516
+ createdAt: text("created_at").notNull().default(now()),
517
+ updatedAt: text("updated_at").notNull().default(now()),
518
+ },
519
+ (receipt) => [
520
+ uniqueIndex(
521
+ "content_database_row_mutation_receipts_database_key_unique",
522
+ ).on(receipt.databaseId, receipt.idempotencyKey),
523
+ index("content_database_row_mutation_receipts_owner_database_idx").on(
524
+ receipt.ownerEmail,
525
+ receipt.databaseId,
526
+ ),
527
+ index("content_database_row_mutation_receipts_document_idx").on(
528
+ receipt.documentId,
529
+ ),
530
+ ],
531
+ );
532
+
497
533
  export const documentPropertyValues = table("document_property_values", {
498
534
  id: text("id").primaryKey(),
499
535
  ownerEmail: text("owner_email").notNull().default("local@localhost"),
@@ -1011,6 +1011,36 @@ export const runContentMigrations = runMigrations(
1011
1011
  ALTER TABLE document_blocks ALTER COLUMN addressable SET DEFAULT true`,
1012
1012
  },
1013
1013
  },
1014
+ {
1015
+ version: 84,
1016
+ name: "content-database-row-mutation-contract",
1017
+ sql: `ALTER TABLE content_databases ADD COLUMN IF NOT EXISTS natural_key_property_id TEXT;
1018
+ CREATE TABLE IF NOT EXISTS content_database_row_mutation_receipts (
1019
+ id TEXT PRIMARY KEY,
1020
+ owner_email TEXT NOT NULL DEFAULT 'local@localhost',
1021
+ org_id TEXT,
1022
+ space_id TEXT NOT NULL,
1023
+ database_id TEXT NOT NULL,
1024
+ database_document_id TEXT NOT NULL,
1025
+ operation TEXT NOT NULL,
1026
+ item_id TEXT NOT NULL,
1027
+ document_id TEXT NOT NULL,
1028
+ idempotency_key TEXT NOT NULL,
1029
+ payload_digest TEXT NOT NULL,
1030
+ schema_revision TEXT NOT NULL,
1031
+ pre_row_revision TEXT,
1032
+ post_row_revision TEXT NOT NULL,
1033
+ result_json TEXT NOT NULL DEFAULT '{}',
1034
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1035
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1036
+ );
1037
+ CREATE UNIQUE INDEX IF NOT EXISTS content_database_row_mutation_receipts_database_key_unique
1038
+ ON content_database_row_mutation_receipts (database_id, idempotency_key);
1039
+ CREATE INDEX IF NOT EXISTS content_database_row_mutation_receipts_owner_database_idx
1040
+ ON content_database_row_mutation_receipts (owner_email, database_id);
1041
+ CREATE INDEX IF NOT EXISTS content_database_row_mutation_receipts_document_idx
1042
+ ON content_database_row_mutation_receipts (document_id)`,
1043
+ },
1014
1044
  ],
1015
1045
  { table: "content_migrations" },
1016
1046
  );
@@ -225,6 +225,7 @@ export interface ConfigureDocumentPropertyRequest {
225
225
  description?: string;
226
226
  visibility?: DocumentPropertyVisibility;
227
227
  options?: DocumentPropertyOptions;
228
+ naturalKey?: boolean;
228
229
  }
229
230
 
230
231
  export interface SetDocumentPropertyRequest {
@@ -258,8 +259,10 @@ export interface ReorderDocumentPropertyRequest {
258
259
  export interface ContentDatabase {
259
260
  id: string;
260
261
  documentId: string;
262
+ spaceId?: string | null;
261
263
  title: string;
262
264
  systemRole?: string | null;
265
+ naturalKeyPropertyId?: string | null;
263
266
  description?: string;
264
267
  viewConfig: ContentDatabaseViewConfig;
265
268
  createdAt: string;
@@ -469,6 +472,62 @@ export interface ContentDatabaseItem {
469
472
  // a secondary source contributes on top of it. Absent for non-federated rows.
470
473
  canonicalKey?: string | null;
471
474
  sourceOverlays?: ContentDatabaseSourceOverlay[];
475
+ rowRevision?: string;
476
+ }
477
+
478
+ export interface ContentDatabaseMutationTarget {
479
+ authorityScope:
480
+ | { kind: "personal"; id: string }
481
+ | { kind: "organization"; id: string };
482
+ spaceId: string;
483
+ databaseId: string;
484
+ databaseDocumentId: string;
485
+ }
486
+
487
+ export interface ContentDatabaseMutationContract {
488
+ target: ContentDatabaseMutationTarget;
489
+ schemaRevision: string;
490
+ naturalKeyPropertyId: string | null;
491
+ properties: Array<{
492
+ id: string;
493
+ name: string;
494
+ type: DocumentPropertyType;
495
+ writable: boolean;
496
+ sourceManaged: boolean;
497
+ acceptedShape: string | null;
498
+ options: DocumentPropertyOptions;
499
+ }>;
500
+ }
501
+
502
+ export interface ContentDatabaseRowMutationReceipt {
503
+ receiptId: string;
504
+ operation: "create" | "update" | "upsert";
505
+ outcome: "created" | "updated" | "unchanged";
506
+ target: ContentDatabaseMutationTarget;
507
+ schemaRevision: string;
508
+ row: {
509
+ itemId: string;
510
+ documentId: string;
511
+ urlPath: string;
512
+ rowRevision: string;
513
+ };
514
+ affected: { title: boolean; propertyIds: string[] };
515
+ idempotency: {
516
+ key: string;
517
+ result: "applied" | "replayed";
518
+ payloadDigest: string;
519
+ };
520
+ revisions: { before: string | null; after: string };
521
+ readback: {
522
+ verified: true;
523
+ title: string;
524
+ propertyValues: Record<string, DocumentPropertyValue>;
525
+ };
526
+ }
527
+
528
+ export interface ContentDatabaseRowMutationResult {
529
+ receipt: ContentDatabaseRowMutationReceipt;
530
+ createdItem?: ContentDatabaseItem;
472
531
  }
473
532
 
474
533
  // A secondary source's read-only contribution to a federated row, matched on the
@@ -828,6 +887,7 @@ export interface ContentDatabaseResponse {
828
887
  removedCount?: number;
829
888
  timings?: BuilderActionTiming[];
830
889
  tableQueryMode?: "server" | "client-required";
890
+ mutationContract?: ContentDatabaseMutationContract;
831
891
  /** Client-only optimistic state while real provider rows are being attached. */
832
892
  attachPreview?: {
833
893
  sourceTable: string;
@@ -892,9 +952,22 @@ export interface CreateInlineDatabaseResponse {
892
952
  }
893
953
 
894
954
  export interface AddDatabaseItemRequest {
895
- databaseId: string;
955
+ target: ContentDatabaseMutationTarget;
956
+ expectedSchemaRevision: string;
957
+ idempotencyKey: string;
896
958
  title?: string;
897
- propertyValues?: Record<string, DocumentPropertyValue>;
959
+ propertyValues?: Record<string, unknown>;
960
+ }
961
+
962
+ export interface UpdateDatabaseItemRequest extends AddDatabaseItemRequest {
963
+ itemId: string;
964
+ documentId: string;
965
+ expectedRowRevision: string;
966
+ }
967
+
968
+ export interface UpsertDatabaseItemByKeyRequest extends AddDatabaseItemRequest {
969
+ keyValue: string;
970
+ expectedRowRevision: string | null;
898
971
  }
899
972
 
900
973
  export interface SubmitContentDatabaseFormRequest {
package/dist/action.d.ts CHANGED
@@ -135,6 +135,28 @@ export interface AgentActionStopOptions {
135
135
  /** Optional short tool-result text. Defaults to the user-facing message. */
136
136
  toolResult?: string;
137
137
  }
138
+ export interface ActionContractErrorOptions {
139
+ /** Stable machine-readable code safe to expose on every action transport. */
140
+ errorCode: string;
141
+ /** Safe structured context for callers. Never include secrets or raw driver errors. */
142
+ details?: Record<string, unknown>;
143
+ /** HTTP status for the action route. Contract conflicts normally use 409. */
144
+ statusCode?: number;
145
+ }
146
+ /**
147
+ * A deterministic, caller-correctable action contract failure.
148
+ *
149
+ * Unlike an ordinary Error, its code and explicitly safe details survive the
150
+ * framework HTTP transport. Internal failures remain generic 500 responses.
151
+ */
152
+ export declare class ActionContractError extends Error {
153
+ readonly actionContractError = true;
154
+ readonly errorCode: string;
155
+ readonly details?: Record<string, unknown>;
156
+ readonly statusCode: number;
157
+ constructor(message: string, options: ActionContractErrorOptions);
158
+ }
159
+ export declare function isActionContractError(error: unknown): error is ActionContractError;
138
160
  /**
139
161
  * Throw from an action when the agent should stop the current turn instead of
140
162
  * feeding the failure back to the model for another retry.
package/dist/action.js CHANGED
@@ -1,5 +1,32 @@
1
1
  import { normalizeActionChatUIConfig, } from "./action-ui.js";
2
2
  import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
3
+ /**
4
+ * A deterministic, caller-correctable action contract failure.
5
+ *
6
+ * Unlike an ordinary Error, its code and explicitly safe details survive the
7
+ * framework HTTP transport. Internal failures remain generic 500 responses.
8
+ */
9
+ export class ActionContractError extends Error {
10
+ actionContractError = true;
11
+ errorCode;
12
+ details;
13
+ statusCode;
14
+ constructor(message, options) {
15
+ super(message);
16
+ this.name = "ActionContractError";
17
+ this.errorCode = options.errorCode;
18
+ this.details = options.details;
19
+ this.statusCode = options.statusCode ?? 409;
20
+ }
21
+ }
22
+ export function isActionContractError(error) {
23
+ return (error instanceof ActionContractError ||
24
+ (!!error &&
25
+ typeof error === "object" &&
26
+ error.actionContractError ===
27
+ true &&
28
+ typeof error.errorCode === "string"));
29
+ }
3
30
  /**
4
31
  * Throw from an action when the agent should stop the current turn instead of
5
32
  * feeding the failure back to the model for another retry.
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -2,7 +2,7 @@ export * from "./config.js";
2
2
  export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, listAgentChatContext, refreshAgentChatContext, removeAgentChatContextItem, sendToAgentChat, sendToAgentChatAndConfirm, setAgentChatContextItem, setContextToAgentChat, isEmbedMcpChatBridgeActive, useAgentChatContext, useAgentChatGenerating, useDevMode, useSendToAgentChat, AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, openMcpAppHostLink, requestMcpAppDisplayMode, sendMcpAppHostMessage, updateMcpAppModelContext, useMcpAppHostContext, CodeRequiredDialog, AgentNativeI18nProvider, LanguagePicker, getLocaleInitScript, localeDirection, normalizeLocaleCode, normalizeLocalePreference, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, useFormatters, useLocale, useT, DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, useDbSync, useFileWatcher, useSession, cn, ApiKeySettings, type AgentChatContextItem, type AgentChatContextMessage, type AgentChatContextMutationOptions, type AgentChatContextRemoveOptions, type AgentChatContextSetOptions, type AgentChatContextState, type AgentChatMessage, type AgentNativeMcpAppHostMessageType, type McpAppDisplayMode, type McpAppHostChatMessage, type McpAppHostCapabilities, type McpAppHostContext, type McpAppHostContextSnapshot, type McpAppModelContextContentPart, type McpAppModelContextUpdate, type CodeRequiredDialogProps, type AuthSession, type UseAgentChatContextResult, type AgentNativeI18nCatalog, type AgentNativeI18nProviderProps, type LocaleCode, type LocaleHydrationPayload, type LocaleMessages, type LocaleMetadata, type LocalePreference, type LocalizationPreference, } from "./client/index.js";
3
3
  export { agentChat } from "./shared/index.js";
4
4
  export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
5
- export { defineAction, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
5
+ export { defineAction, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
6
6
  export { normalizeUserProfileName, USER_PROFILE_SETTING_KEY, type UserProfile, } from "./user-profile/index.js";
7
7
  export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, type ActionChatUIConfig, } from "./action-ui.js";
8
8
  export { DATA_CHART_WIDGET, DATA_INSIGHTS_WIDGET, DATA_TABLE_WIDGET, createDataChartWidgetResult, createDataInsightsWidgetResult, createDataTableWidgetResult, dataChartWidgetResultSchema, dataChartWidgetSchema, dataInsightsWidgetResultSchema, dataTableWidgetResultSchema, dataTableWidgetSchema, dataWidgetResultSchema, isDataChartWidget, isDataTableWidget, isDataWidgetResult, normalizeDataWidgetKind, normalizeDataWidgetResult, type DataChartSeriesDefinition, type DataChartWidget, type DataChartWidgetResult, type DataChartWidgetResultInput, type DataInsightsWidgetResult, type DataInsightsWidgetResultInput, type DataTableColumn, type DataTableWidget, type DataTableWidgetResult, type DataTableWidgetResultInput, type DataWidgetDisplay, type DataWidgetKind, type DataWidgetResult, type DataWidgetResultMetadata, } from "./data-widgets/index.js";
@@ -9,7 +9,7 @@ export { agentChat } from "./shared/index.js";
9
9
  // Pure utilities (no Node.js deps — safe for browser and SSR)
10
10
  export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
11
11
  // defineAction — used by template actions, no Node.js deps
12
- export { defineAction, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
12
+ export { defineAction, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
13
13
  export { normalizeUserProfileName, USER_PROFILE_SETTING_KEY, } from "./user-profile/index.js";
14
14
  export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, } from "./action-ui.js";
15
15
  export { DATA_CHART_WIDGET, DATA_INSIGHTS_WIDGET, DATA_TABLE_WIDGET, createDataChartWidgetResult, createDataInsightsWidgetResult, createDataTableWidgetResult, dataChartWidgetResultSchema, dataChartWidgetSchema, dataInsightsWidgetResultSchema, dataTableWidgetResultSchema, dataTableWidgetSchema, dataWidgetResultSchema, isDataChartWidget, isDataTableWidget, isDataWidgetResult, normalizeDataWidgetKind, normalizeDataWidgetResult, } from "./data-widgets/index.js";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from "./config.js";
2
2
  export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type ActionTool, type ScriptTool, type AgentMessage, type AgentChatRequest, type AgentChatEvent, type AgentChatAttachment, DEFAULT_MODEL, TripWire, type Processor, type ProcessorState, type ProcessorAbort, } from "./agent/index.js";
3
- export { defineAction, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
3
+ export { defineAction, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
4
4
  export * from "./user-profile/index.js";
5
5
  export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, type EmbedAppOptions, } from "./mcp/embed-app.js";
6
6
  export { embedRoute, type EmbedRouteContext, type EmbedRouteOptions, type EmbedRoutePathBuilder, type EmbedRouteResult, } from "./mcp/embed-route.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export * from "./config.js";
5
5
  export { createProductionAgentHandler, DEFAULT_MODEL,
6
6
  // In-loop processors (observers/guardrails for runAgentLoop)
7
7
  TripWire, } from "./agent/index.js";
8
- export { defineAction, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
8
+ export { defineAction, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
9
9
  export * from "./user-profile/index.js";
10
10
  export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, } from "./mcp/embed-app.js";
11
11
  export { embedRoute, } from "./mcp/embed-route.js";
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ ok?: undefined;
45
+ error?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
47
- error?: undefined;
48
- ok?: undefined;
49
49
  } | {
50
+ ok?: undefined;
51
+ error?: undefined;
50
52
  summary?: undefined;
51
53
  spans?: undefined;
52
54
  id: string;
53
- error?: undefined;
54
- ok?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  id?: undefined;
59
60
  error: any;
60
- ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -1,6 +1,6 @@
1
1
  import { createError, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getQuery, getHeader, getRequestURL, } from "h3";
2
2
  import { verifyA2ATokenWithClaims } from "../a2a-claims.js";
3
- import { isAgentActionStopError } from "../action.js";
3
+ import { isActionContractError, isAgentActionStopError } from "../action.js";
4
4
  import { declaresFeatureFlagDelegation } from "../feature-flags/a2a-action-route.js";
5
5
  import { resolveOrgByDomain, resolveOrgIdForEmail } from "../org/context.js";
6
6
  import { readBody } from "../server/h3-helpers.js";
@@ -507,7 +507,15 @@ export function mountActionRoutes(nitroApp, actions, options) {
507
507
  isAgentActionStopError(err) ||
508
508
  (explicitStatus !== undefined && explicitStatus < 500);
509
509
  if (isUserFacing) {
510
- return { error: msg };
510
+ return isActionContractError(err)
511
+ ? {
512
+ error: msg,
513
+ errorCode: err.errorCode,
514
+ ...(err.details === undefined
515
+ ? {}
516
+ : { details: err.details }),
517
+ }
518
+ : { error: msg };
511
519
  }
512
520
  const requestId = getHttpRequestTelemetryId(event);
513
521
  const captureId = captureError(err, {