@agent-native/core 0.174.1 → 0.174.2-nightly-20260825214843

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/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8614
34
+ - template files: 8615
@@ -68,6 +68,10 @@ Each field is a JSON object:
68
68
  - `label` — display label
69
69
  - `required` — boolean
70
70
 
71
+ Always pass fields as these complete objects. Never encode a field as shorthand
72
+ text such as `text: Enter a name`; `patch-form-fields` upserts also require the
73
+ field `id` so they cannot be ambiguous.
74
+
71
75
  ### Optional properties
72
76
  - `placeholder` — input placeholder text
73
77
  - `description` — help text below the field
@@ -49,7 +49,9 @@ export default defineAction({
49
49
  fields: z
50
50
  .union([z.string(), z.array(z.any())])
51
51
  .optional()
52
- .describe("Array of form fields (or JSON string of the same)"),
52
+ .describe(
53
+ "Array of complete field objects with id, type, label, and required (or JSON string of the same); never use shorthand strings such as 'text: Enter a name'.",
54
+ ),
53
55
  settings: z
54
56
  .union([z.string(), z.record(z.string(), z.any())])
55
57
  .optional()
@@ -24,7 +24,10 @@ import { z } from "zod";
24
24
  import { getDb, schema } from "../server/db/index.js";
25
25
  import { applyFieldOps } from "../server/lib/merge-fields.js";
26
26
  import { invalidatePublicFormCache } from "../server/lib/public-form-ssr.js";
27
- import { assertValidFields } from "../server/lib/validate-fields.js";
27
+ import {
28
+ assertValidFields,
29
+ normalizePersistedFields,
30
+ } from "../server/lib/validate-fields.js";
28
31
  import type { FormField } from "../shared/types.js";
29
32
  import { assertPublishableForm } from "./lib/assert-publishable-form.js";
30
33
 
@@ -61,7 +64,11 @@ export function withFormLock<T>(
61
64
  const fieldOpSchema = z.union([
62
65
  z.object({
63
66
  op: z.literal("upsert"),
64
- field: z.record(z.string(), z.any()),
67
+ field: z
68
+ .record(z.string(), z.any())
69
+ .describe(
70
+ "Complete field object with id, type, label, and required; never use shorthand strings.",
71
+ ),
65
72
  }),
66
73
  z.object({
67
74
  op: z.literal("remove"),
@@ -117,7 +124,9 @@ export default defineAction({
117
124
  // Parse current fields from the DB row.
118
125
  let currentFields: FormField[] = [];
119
126
  try {
120
- currentFields = JSON.parse(existing.fields) as FormField[];
127
+ currentFields = normalizePersistedFields(
128
+ JSON.parse(existing.fields),
129
+ ) as FormField[];
121
130
  } catch {
122
131
  currentFields = [];
123
132
  }
@@ -35,7 +35,9 @@ export default defineAction({
35
35
  fields: z
36
36
  .union([z.string(), z.array(z.any())])
37
37
  .optional()
38
- .describe("Array of form fields (or JSON string of the same)"),
38
+ .describe(
39
+ "Array of complete field objects with id, type, label, and required (or JSON string of the same); never use shorthand strings such as 'text: Enter a name'.",
40
+ ),
39
41
  settings: z
40
42
  .union([z.string(), z.record(z.string(), z.any())])
41
43
  .optional()
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-25
4
+ ---
5
+ Forms agents now retry rejected field edits with valid field objects.
@@ -4,6 +4,19 @@
4
4
  // runtime — an unrestricted id like `x" onfocus="alert(1)` would otherwise
5
5
  // stored-XSS every anonymous submitter of a published form.
6
6
  export const FIELD_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
7
+ const FIELD_TYPES = new Set([
8
+ "text",
9
+ "email",
10
+ "number",
11
+ "textarea",
12
+ "select",
13
+ "multiselect",
14
+ "checkbox",
15
+ "radio",
16
+ "date",
17
+ "rating",
18
+ "scale",
19
+ ]);
7
20
  const CONDITIONAL_OPERATORS = new Set(["equals", "not_equals", "contains"]);
8
21
 
9
22
  /**
@@ -49,6 +62,28 @@ export function normalizeFieldIds(fields: unknown): unknown {
49
62
  });
50
63
  }
51
64
 
65
+ /**
66
+ * Keeps granular edits compatible with fields written before the current
67
+ * schema. The UI already renders unknown types as text and treats a missing
68
+ * required flag as false, so patch reads must make the same repair before the
69
+ * strict persistence check runs.
70
+ */
71
+ export function normalizePersistedFields(fields: unknown): unknown {
72
+ if (!Array.isArray(fields)) return fields;
73
+ return fields.map((field) => {
74
+ if (field == null || typeof field !== "object" || Array.isArray(field)) {
75
+ return field;
76
+ }
77
+ const f = field as Record<string, unknown>;
78
+ return {
79
+ ...f,
80
+ type:
81
+ typeof f.type === "string" && FIELD_TYPES.has(f.type) ? f.type : "text",
82
+ required: f.required === undefined ? false : f.required,
83
+ };
84
+ });
85
+ }
86
+
52
87
  export function assertValidFields(fields: unknown): void {
53
88
  if (!Array.isArray(fields)) {
54
89
  throw new Error("fields must be an array");
@@ -71,6 +106,18 @@ export function assertValidFields(fields: unknown): void {
71
106
  }
72
107
  seenIds.add(id);
73
108
 
109
+ if (typeof f.type !== "string" || !FIELD_TYPES.has(f.type)) {
110
+ throw new Error(
111
+ `field #${idx + 1} has an invalid type ${JSON.stringify(f.type)}`,
112
+ );
113
+ }
114
+ if (typeof f.label !== "string") {
115
+ throw new Error(`field #${idx + 1} label must be a string`);
116
+ }
117
+ if (typeof f.required !== "boolean") {
118
+ throw new Error(`field #${idx + 1} required must be a boolean`);
119
+ }
120
+
74
121
  const cond = f.conditional;
75
122
  if (cond !== undefined) {
76
123
  if (cond == null || typeof cond !== "object") {
@@ -11,6 +11,7 @@ export const FORMS_SYSTEM_PROMPT = `## Agent-Native Forms
11
11
  You are the in-app agent for Agent-Native Forms, a chat-first form builder and response workspace. Help users create, edit, publish, share, and analyze forms through the registered actions. The actions are the source of truth for database access, validation, permissions, UI sync, and business logic.
12
12
 
13
13
  Core rules:
14
+ - Build every field as a complete object with id, type, label, and required; never send shorthand strings such as "text: Enter a name". If a form or field action rejects its payload, correct the arguments and retry in the same turn, then verify the saved fields. If a draft was created before the failure, repair that draft instead of explaining the fix and stopping.
14
15
  - Use Forms actions instead of raw database access. Raw database tools are not available in this app.
15
16
  - Never hardcode API keys, tokens, webhook URLs, signing secrets, customer data, or credential-looking literals. Use placeholders or configured integrations.
16
17
  - Inspect the current state when it matters. Use \`view-screen\` when the active form, selected field, publish state, response table, or current route is unclear.
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
+ error?: undefined;
20
21
  configured?: undefined;
21
22
  connectPath?: undefined;
22
23
  url: string;
23
24
  id: string;
24
25
  provider: string;
25
- error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
45
+ ok?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
49
+ } | {
47
50
  error?: undefined;
48
51
  ok?: undefined;
49
- } | {
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
66
  ok: boolean;
66
- error?: undefined;
67
67
  }>>;
@@ -34,37 +34,37 @@ 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
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
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
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -34,10 +34,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
34
34
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
35
35
  error: any;
36
36
  } | {
37
+ error?: undefined;
37
38
  ok: boolean;
38
39
  key: string;
39
40
  baseUrlKey?: string;
40
41
  scope: AgentEngineApiKeyScope;
41
- error?: undefined;
42
42
  }>>;
43
43
  export {};
@@ -2593,7 +2593,10 @@ ${signInJourneyInlineScript()}
2593
2593
  }
2594
2594
  }
2595
2595
  function __anFinishOAuthExchange(ret, flowId, sessionToken) {
2596
+ __anStopOAuthPopupWatch();
2597
+ __anStopNativeOAuthAbandonment();
2596
2598
  __anGoogleSignInInFlight = false;
2599
+ __anClearGoogleSignInFlow();
2597
2600
  __anMagicLinkInFlight = false;
2598
2601
  if (__anIsBuilderPreview()) {
2599
2602
  if (sessionToken) {
@@ -2863,8 +2866,18 @@ ${identitySsoScript}
2863
2866
  return __anIsAgentNativeDesktop() ? 'redirect' : 'popup';
2864
2867
  }
2865
2868
  var __anOAuthPollTimer = null;
2869
+ var __anOAuthPopupWatchTimer = null;
2870
+ var __anOAuthPopupCloseGraceTimer = null;
2871
+ var __anOAuthPopupCloseGraceMs = 5000;
2872
+ var __anNativeOAuthFlowId = null;
2873
+ var __anNativeOAuthRequestPending = false;
2874
+ var __anNativeOAuthReturnObserved = false;
2875
+ var __anNativeOAuthAbandonGraceTimer = null;
2876
+ var __anNativeOAuthAbandonGraceMs = 5000;
2866
2877
  var __anOAuthPollCount = 0;
2867
2878
  var __anGoogleSignInInFlight = false;
2879
+ var __anGoogleSignInFlowId = null;
2880
+ var __anGoogleSignInRecoveryFlowId = null;
2868
2881
  var __anMagicLinkInFlight = false;
2869
2882
  var __anGoogleRecoverBound = false;
2870
2883
  function __anNewOAuthFlowId() {
@@ -2921,8 +2934,139 @@ ${identitySsoScript}
2921
2934
  __anOAuthPollTimer = null;
2922
2935
  }
2923
2936
  }
2937
+ function __anStopOAuthPopupWatch() {
2938
+ if (__anOAuthPopupWatchTimer) {
2939
+ clearInterval(__anOAuthPopupWatchTimer);
2940
+ __anOAuthPopupWatchTimer = null;
2941
+ }
2942
+ if (__anOAuthPopupCloseGraceTimer) {
2943
+ clearTimeout(__anOAuthPopupCloseGraceTimer);
2944
+ __anOAuthPopupCloseGraceTimer = null;
2945
+ }
2946
+ }
2947
+ function __anStopNativeOAuthAbandonment() {
2948
+ __anCancelNativeOAuthAbandonment();
2949
+ __anNativeOAuthFlowId = null;
2950
+ __anNativeOAuthRequestPending = false;
2951
+ }
2952
+ function __anCancelNativeOAuthAbandonment() {
2953
+ if (__anNativeOAuthAbandonGraceTimer) {
2954
+ clearTimeout(__anNativeOAuthAbandonGraceTimer);
2955
+ __anNativeOAuthAbandonGraceTimer = null;
2956
+ }
2957
+ __anNativeOAuthReturnObserved = false;
2958
+ }
2959
+ function __anBeginNativeOAuth(flowId) {
2960
+ __anStopNativeOAuthAbandonment();
2961
+ __anNativeOAuthFlowId = flowId;
2962
+ __anNativeOAuthRequestPending = true;
2963
+ __anNativeOAuthReturnObserved = false;
2964
+ }
2965
+ function __anMarkNativeOAuthPolling(flowId) {
2966
+ if (__anNativeOAuthFlowId !== flowId) return;
2967
+ __anNativeOAuthRequestPending = false;
2968
+ }
2969
+ function __anIsCurrentNativeOAuth(flowId) {
2970
+ return __anNativeOAuthFlowId === flowId && __anIsCurrentGoogleSignInFlow(flowId);
2971
+ }
2972
+ function __anFinalizeNativeOAuthAbandonment(flowId) {
2973
+ if (!__anIsCurrentNativeOAuth(flowId) || !__anOAuthPollTimer) return;
2974
+ if (document.visibilityState === 'hidden') {
2975
+ __anCancelNativeOAuthAbandonment();
2976
+ return;
2977
+ }
2978
+ __anStopNativeOAuthAbandonment();
2979
+ if (__anInvalidateGoogleSignInFlow(flowId)) {
2980
+ __anStopOAuthExchangePolling();
2981
+ __anRecoverGoogleSignInAfterReturn(flowId);
2982
+ }
2983
+ }
2984
+ function __anScheduleNativeOAuthAbandonment(flowId) {
2985
+ // Returning focus is the only signal that the system-browser ceremony was
2986
+ // abandoned. Preserve the exchange poll briefly for a late deep-link.
2987
+ if (
2988
+ !__anIsCurrentNativeOAuth(flowId) ||
2989
+ __anNativeOAuthRequestPending ||
2990
+ !__anNativeOAuthReturnObserved ||
2991
+ !__anOAuthPollTimer ||
2992
+ __anNativeOAuthAbandonGraceTimer
2993
+ ) return;
2994
+ __anNativeOAuthAbandonGraceTimer = setTimeout(function() {
2995
+ __anNativeOAuthAbandonGraceTimer = null;
2996
+ __anFinalizeNativeOAuthAbandonment(flowId);
2997
+ }, __anNativeOAuthAbandonGraceMs);
2998
+ }
2999
+ function __anBeginGoogleSignInFlow(flowId) {
3000
+ __anGoogleSignInFlowId = flowId;
3001
+ __anGoogleSignInRecoveryFlowId = null;
3002
+ __anGoogleSignInInFlight = true;
3003
+ }
3004
+ function __anIsCurrentGoogleSignInFlow(flowId) {
3005
+ return __anGoogleSignInInFlight && __anGoogleSignInFlowId === flowId;
3006
+ }
3007
+ function __anClearGoogleSignInFlow() {
3008
+ __anGoogleSignInFlowId = null;
3009
+ __anGoogleSignInRecoveryFlowId = null;
3010
+ }
3011
+ function __anInvalidateGoogleSignInFlow(flowId) {
3012
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return false;
3013
+ __anGoogleSignInFlowId = null;
3014
+ __anGoogleSignInRecoveryFlowId = flowId;
3015
+ return true;
3016
+ }
3017
+ function __anCanRecoverGoogleSignInFlow(flowId, expectedFlowId) {
3018
+ // Focus can return while either the system-browser exchange or the popup
3019
+ // exchange is still active. Let that exchange finish before recovering.
3020
+ if (!flowId && (__anNativeOAuthFlowId || __anOAuthPollTimer || __anOAuthPopupWatchTimer)) return false;
3021
+ if (flowId) {
3022
+ return __anGoogleSignInInFlight &&
3023
+ !__anGoogleSignInFlowId &&
3024
+ __anGoogleSignInRecoveryFlowId === flowId;
3025
+ }
3026
+ return __anGoogleSignInInFlight && __anGoogleSignInFlowId === expectedFlowId;
3027
+ }
3028
+ function __anFinalizeOAuthPopupClose(flowId) {
3029
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3030
+ __anOAuthPopupCloseGraceTimer = null;
3031
+ if (__anInvalidateGoogleSignInFlow(flowId)) {
3032
+ __anStopOAuthExchangePolling();
3033
+ __anRecoverGoogleSignInAfterReturn(flowId);
3034
+ }
3035
+ }
3036
+ function __anHandleOAuthPopupClosed(flowId) {
3037
+ __anStopOAuthPopupWatch();
3038
+ // The success callback closes its tab 250ms after staging the token, but
3039
+ // the opener polls once per second. Let an active exchange win that race.
3040
+ if (__anOAuthPollTimer) {
3041
+ __anOAuthPopupCloseGraceTimer = setTimeout(function() {
3042
+ __anFinalizeOAuthPopupClose(flowId);
3043
+ }, __anOAuthPopupCloseGraceMs);
3044
+ return;
3045
+ }
3046
+ __anFinalizeOAuthPopupClose(flowId);
3047
+ }
3048
+ function __anWatchOAuthPopupClose(popup, flowId) {
3049
+ __anStopOAuthPopupWatch();
3050
+ __anOAuthPopupWatchTimer = setInterval(function() {
3051
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) {
3052
+ __anStopOAuthPopupWatch();
3053
+ return;
3054
+ }
3055
+ var closed = false;
3056
+ try {
3057
+ closed = popup.closed === true;
3058
+ } catch(e) {
3059
+ __anHandleOAuthPopupClosed(flowId);
3060
+ return;
3061
+ }
3062
+ if (!closed) return;
3063
+ __anHandleOAuthPopupClosed(flowId);
3064
+ }, 500);
3065
+ }
2924
3066
  function __anShowAuthExchangeError(err, btn, message, kind) {
2925
3067
  __anStopOAuthExchangePolling();
3068
+ __anStopOAuthPopupWatch();
3069
+ __anStopNativeOAuthAbandonment();
2926
3070
  err.textContent = message;
2927
3071
  err.classList.add('show', 'error');
2928
3072
  btn.disabled = false;
@@ -2931,22 +3075,30 @@ ${identitySsoScript}
2931
3075
  showMagicLinkForm();
2932
3076
  } else {
2933
3077
  __anGoogleSignInInFlight = false;
3078
+ __anClearGoogleSignInFlow();
2934
3079
  }
2935
3080
  }
2936
3081
  function __anShowOAuthError(err, btn, message) {
2937
3082
  __anShowAuthExchangeError(err, btn, message, 'google');
2938
3083
  }
2939
- function __anRecoverGoogleSignInAfterReturn() {
3084
+ function __anRecoverGoogleSignInAfterReturn(flowId) {
2940
3085
  // The user left for the Google sign-in window and came back. If the flow
2941
3086
  // never completed (e.g. they closed the window to switch profiles), the
2942
3087
  // button is stuck disabled with no error path firing for up to 5 minutes.
2943
3088
  // Re-enable it so they can retry. Wait briefly first so a genuinely
2944
3089
  // in-flight exchange can still finish and navigate without a flicker.
2945
- if (!__anGoogleSignInInFlight) return;
3090
+ if (!flowId && __anNativeOAuthFlowId) {
3091
+ __anNativeOAuthReturnObserved = true;
3092
+ __anScheduleNativeOAuthAbandonment(__anNativeOAuthFlowId);
3093
+ return;
3094
+ }
3095
+ var expectedFlowId = flowId || __anGoogleSignInFlowId;
3096
+ if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
2946
3097
  setTimeout(function() {
3098
+ if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
2947
3099
  __anMaybeRedirectSignedIn(__anResumeHref()).then(function(redirected) {
3100
+ if (!__anCanRecoverGoogleSignInFlow(flowId, expectedFlowId)) return;
2948
3101
  if (redirected) return;
2949
- if (!__anGoogleSignInInFlight) return;
2950
3102
  var btn = document.getElementById('google-btn');
2951
3103
  if (!btn || !btn.disabled) return;
2952
3104
  // Keep the desktop-exchange poll alive. Agent Native Desktop opens
@@ -2954,15 +3106,25 @@ ${identitySsoScript}
2954
3106
  // callback has stored the session token.
2955
3107
  btn.disabled = false;
2956
3108
  __anGoogleSignInInFlight = false;
3109
+ __anClearGoogleSignInFlow();
2957
3110
  });
2958
3111
  }, 1200);
2959
3112
  }
2960
3113
  function __anBindGoogleRecover() {
2961
3114
  if (__anGoogleRecoverBound) return;
2962
3115
  __anGoogleRecoverBound = true;
2963
- window.addEventListener('focus', __anRecoverGoogleSignInAfterReturn);
3116
+ window.addEventListener('focus', function() {
3117
+ __anRecoverGoogleSignInAfterReturn();
3118
+ });
3119
+ window.addEventListener('blur', function() {
3120
+ __anCancelNativeOAuthAbandonment();
3121
+ });
2964
3122
  document.addEventListener('visibilitychange', function() {
2965
- if (document.visibilityState === 'visible') __anRecoverGoogleSignInAfterReturn();
3123
+ if (document.visibilityState === 'visible') {
3124
+ __anRecoverGoogleSignInAfterReturn();
3125
+ } else {
3126
+ __anCancelNativeOAuthAbandonment();
3127
+ }
2966
3128
  });
2967
3129
  }
2968
3130
  function __anHandlePopupOAuthFailure(ret, btn, err, flowId, redirectReason, builderFrameMessage) {
@@ -2990,6 +3152,7 @@ ${identitySsoScript}
2990
3152
  var isMagicLink = kind === 'magic-link';
2991
3153
  __anOAuthPollCount = 0;
2992
3154
  async function check() {
3155
+ if (!isMagicLink && !__anIsCurrentGoogleSignInFlow(flowId)) return;
2993
3156
  __anOAuthPollCount++;
2994
3157
  try {
2995
3158
  var exchangeParams = '?flow_id=' + encodeURIComponent(flowId);
@@ -2999,6 +3162,7 @@ ${identitySsoScript}
2999
3162
  }
3000
3163
  var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + exchangeParams, exchangeOptions);
3001
3164
  var data = await res.json().catch(function() { return {}; });
3165
+ if (!isMagicLink && !__anIsCurrentGoogleSignInFlow(flowId)) return;
3002
3166
  if (data && (data.email || data.token)) {
3003
3167
  if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);
3004
3168
  __anOAuthPollTimer = null;
@@ -3062,8 +3226,7 @@ ${identitySsoScript}
3062
3226
  }
3063
3227
  return data.url;
3064
3228
  }
3065
- function __anStartPopupOAuth(ret, btn, err) {
3066
- var flowId = __anNewOAuthFlowId();
3229
+ function __anStartPopupOAuth(ret, btn, err, flowId) {
3067
3230
  var verifier = __anNewOAuthVerifier();
3068
3231
  if (!verifier) {
3069
3232
  __anShowOAuthError(err, btn, __anT('failedToConnect'));
@@ -3080,11 +3243,13 @@ ${identitySsoScript}
3080
3243
  return;
3081
3244
  }
3082
3245
  try { popup.opener = null; } catch(e) {}
3246
+ __anWatchOAuthPopupClose(popup, flowId);
3083
3247
  } catch(e) {
3084
3248
  __anHandlePopupOAuthFailure(ret, btn, err, flowId, 'Could not open Google popup; falling back to redirect', 'Could not open Google popup.');
3085
3249
  return;
3086
3250
  }
3087
3251
  __anRequestDesktopOAuthUrl(flowId, verifier, oauthReturn).then(function(url) {
3252
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3088
3253
  try {
3089
3254
  popup.location.href = url;
3090
3255
  __anSetOAuthDebug('Google popup opened; waiting for callback', flowId);
@@ -3093,24 +3258,29 @@ ${identitySsoScript}
3093
3258
  throw e;
3094
3259
  }
3095
3260
  }).catch(function(e) {
3261
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3096
3262
  try { popup.close(); } catch(closeErr) {}
3097
3263
  __anShowOAuthError(err, btn, e && e.message ? e.message : __anT('failedToConnect'));
3098
3264
  });
3099
3265
  }
3100
- function __anStartNativeDesktopOAuth(ret, btn, err) {
3101
- var flowId = __anNewOAuthFlowId();
3266
+ function __anStartNativeDesktopOAuth(ret, btn, err, flowId) {
3102
3267
  var verifier = __anNewOAuthVerifier();
3103
3268
  if (!verifier) {
3104
3269
  __anShowOAuthError(err, btn, __anT('failedToConnect'));
3105
3270
  return;
3106
3271
  }
3107
3272
  var oauthReturn = ret;
3273
+ __anBeginNativeOAuth(flowId);
3108
3274
  __anSetOAuthDebug('Preparing Google sign-in in system browser', flowId);
3109
3275
  __anRequestDesktopOAuthUrl(flowId, verifier, oauthReturn).then(function(url) {
3276
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3277
+ __anMarkNativeOAuthPolling(flowId);
3110
3278
  __anSetOAuthDebug('Opening Google sign-in in system browser', flowId);
3111
3279
  __anOpenOAuthUrl(url);
3112
3280
  __anWaitForOAuthExchange(flowId, ret, btn, err, 'google', verifier);
3281
+ __anScheduleNativeOAuthAbandonment(flowId);
3113
3282
  }).catch(function(e) {
3283
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3114
3284
  __anShowOAuthError(err, btn, e && e.message ? e.message : __anT('failedToConnect'));
3115
3285
  });
3116
3286
  }
@@ -3831,20 +4001,20 @@ ${renderGoogleButton
3831
4001
  var btn = document.getElementById('google-btn');
3832
4002
  var err = document.getElementById('google-err');
3833
4003
  var ret = __anResumeHref();
4004
+ var flowId = __anNewOAuthFlowId();
3834
4005
  btn.disabled = true;
3835
- __anGoogleSignInInFlight = true;
4006
+ __anBeginGoogleSignInFlow(flowId);
3836
4007
  __anBindGoogleRecover();
3837
4008
  err.classList.remove('show');
3838
4009
  if (__anResolveAuthFlow() === 'popup') {
3839
- __anStartPopupOAuth(ret, btn, err);
4010
+ __anStartPopupOAuth(ret, btn, err, flowId);
3840
4011
  return;
3841
4012
  }
3842
4013
  if (__anIsAgentNativeDesktop()) {
3843
- __anStartNativeDesktopOAuth(ret, btn, err);
4014
+ __anStartNativeDesktopOAuth(ret, btn, err, flowId);
3844
4015
  return;
3845
4016
  }
3846
4017
  if (__anIsBuilderPreview()) {
3847
- var flowId = __anNewOAuthFlowId();
3848
4018
  __anStartRedirectOAuth(ret, btn, err, flowId, 'Opening Google sign-in redirect from Builder preview');
3849
4019
  return;
3850
4020
  }
@@ -3852,6 +4022,7 @@ ${renderGoogleButton
3852
4022
  var authUrl = __anGoogleAuthUrlPath() + '?return=' + encodeURIComponent(ret);
3853
4023
  var res = await fetch(authUrl);
3854
4024
  var data = await res.json();
4025
+ if (!__anIsCurrentGoogleSignInFlow(flowId)) return;
3855
4026
  if (data.url) {
3856
4027
  __anOpenOAuthUrl(data.url);
3857
4028
  } else {
@@ -3859,12 +4030,14 @@ ${renderGoogleButton
3859
4030
  err.classList.add('show');
3860
4031
  btn.disabled = false;
3861
4032
  __anGoogleSignInInFlight = false;
4033
+ __anClearGoogleSignInFlow();
3862
4034
  }
3863
4035
  } catch (e) {
3864
4036
  err.textContent = __anT('failedToConnect');
3865
4037
  err.classList.add('show');
3866
4038
  btn.disabled = false;
3867
4039
  __anGoogleSignInInFlight = false;
4040
+ __anClearGoogleSignInFlow();
3868
4041
  }
3869
4042
  }`
3870
4043
  : ""}
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
@@ -2254,7 +2254,11 @@ function isNitroEnvironmentUnavailable(error) {
2254
2254
  typeof candidate.message === "string" &&
2255
2255
  /Vite environment .+ is unavailable/.test(candidate.message));
2256
2256
  }
2257
- const NITRO_STARTUP_SETTLE_MS = 1_000;
2257
+ // Nitro's worker can expose a transformed module graph before its entry
2258
+ // module has finished importing. Keep the recovery page up for the same
2259
+ // cold-start window instead of letting the first poll render a 500 error.
2260
+ const NITRO_STARTUP_SETTLE_MS = 3_000;
2261
+ const NITRO_STARTUP_POLL_INTERVAL_MS = 100;
2258
2262
  const NITRO_STARTUP_TIMEOUT_MS = 30_000;
2259
2263
  const NITRO_STARTUP_RETRY_DELAY_MS = 1_000;
2260
2264
  const NITRO_STARTUP_RETRY_MAX_DELAY_MS = 3_000;
@@ -2366,15 +2370,24 @@ function nitroStartupGate(options = {}) {
2366
2370
  let graphSignature = null;
2367
2371
  let graphStableAt;
2368
2372
  let startupComplete = false;
2369
- server.middlewares.use((req, res, next) => {
2370
- if (startupComplete || !isHtmlDocumentRequest(req)) {
2371
- next();
2372
- return;
2373
+ let readinessTimer;
2374
+ const completeStartup = () => {
2375
+ startupComplete = true;
2376
+ if (readinessTimer) {
2377
+ clearInterval(readinessTimer);
2378
+ readinessTimer = undefined;
2373
2379
  }
2380
+ };
2381
+ // Observe the graph independently of the first document request. A
2382
+ // server can finish compiling while the shell is still on its loading
2383
+ // screen; measuring stability only from the first request adds the full
2384
+ // settle window to an otherwise-ready server.
2385
+ const observeStartup = () => {
2386
+ if (startupComplete)
2387
+ return;
2374
2388
  const timestamp = now();
2375
2389
  if (timestamp - startedAt >= timeoutMs) {
2376
- startupComplete = true;
2377
- next();
2390
+ completeStartup();
2378
2391
  return;
2379
2392
  }
2380
2393
  const nextGraphSignature = nitroModuleGraphSignature(server.environments?.nitro);
@@ -2385,15 +2398,43 @@ function nitroStartupGate(options = {}) {
2385
2398
  }
2386
2399
  else if (graphStableAt !== undefined &&
2387
2400
  timestamp - graphStableAt >= settleMs) {
2388
- startupComplete = true;
2389
- next();
2390
- return;
2401
+ completeStartup();
2391
2402
  }
2392
2403
  }
2393
2404
  else {
2394
2405
  graphSignature = null;
2395
2406
  graphStableAt = undefined;
2396
2407
  }
2408
+ };
2409
+ // Start watching before any HTML request arrives so readiness time is
2410
+ // measured from server startup, not from the user's first navigation.
2411
+ observeStartup();
2412
+ if (!startupComplete) {
2413
+ readinessTimer = setInterval(observeStartup, NITRO_STARTUP_POLL_INTERVAL_MS);
2414
+ readinessTimer.unref?.();
2415
+ server.httpServer?.once("close", () => {
2416
+ if (readinessTimer) {
2417
+ clearInterval(readinessTimer);
2418
+ readinessTimer = undefined;
2419
+ }
2420
+ });
2421
+ }
2422
+ server.middlewares.use((req, res, next) => {
2423
+ if (startupComplete || !isHtmlDocumentRequest(req)) {
2424
+ next();
2425
+ return;
2426
+ }
2427
+ const timestamp = now();
2428
+ if (timestamp - startedAt >= timeoutMs) {
2429
+ completeStartup();
2430
+ next();
2431
+ return;
2432
+ }
2433
+ observeStartup();
2434
+ if (startupComplete) {
2435
+ next();
2436
+ return;
2437
+ }
2397
2438
  sendNitroStartingResponse(req, res);
2398
2439
  });
2399
2440
  },
@@ -214,7 +214,7 @@ Four steps take you from template to deployed app:
214
214
 
215
215
  1. **Pick an app.** Use the CLI picker, or browse the docs.
216
216
  2. **Brand it.** Change the name, colors, logo, and copy. Most example apps expose this in a single config file.
217
- 3. **Customize it.** Make source changes in your own repository and development workflow. An agent can help when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions, data, and configured integrations.
217
+ 3. **Customize it.** Make source changes in your own repository and development workflow: "add a priority field to forms," "connect to our Salesforce instance," "change the color scheme to match our brand." An agent can help when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions, data, and configured integrations.
218
218
  4. **Ship it.** Run the deploy command. You now have a production app at your own domain.
219
219
 
220
220
  Steps 2–4 typically take a few days. Step 3 has no fixed end point — the app is yours to evolve, while the embedded agent remains available to operate the deployed product and can assist with source changes when code access is explicitly enabled.
@@ -168,6 +168,13 @@ multi-user apps should prefer the scoped credential and connection store.
168
168
  Every provider key below selects its engine on its own: dropping one in is
169
169
  enough, and `AGENT_ENGINE` is only needed to pick between several.
170
170
 
171
+ Supported models span Anthropic Claude, OpenAI (GPT-5 family), Google Gemini,
172
+ and any provider that speaks the OpenAI API shape via `OPENAI_BASE_URL`,
173
+ including OpenAI-compatible gateways like LiteLLM. The model is chosen in
174
+ Settings, so switching providers is a configuration change, not a code
175
+ rewrite. Claude is the framework's most heavily tested path and the default
176
+ recommendation.
177
+
171
178
  | Variable | What it controls | Supported values or format |
172
179
  | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------- |
173
180
  | `ANTHROPIC_API_KEY` | Claude model fallback for the built-in AI engine. | An Anthropic API key. |
@@ -370,6 +370,8 @@ Every consumer calls the same underlying function, so there is only one implemen
370
370
 
371
371
  When the agent changes data, the UI needs to reflect that without a manual refresh. `useDbSync()` is what makes that automatic. Same-process writes stream over `/_agent-native/events`; `/_agent-native/poll` remains the cross-process and serverless fallback. When the agent writes to the database (application state, settings, or domain data), a version counter increments and the client invalidates the relevant React Query caches.
372
372
 
373
+ SSE covers the common case instead of WebSockets because it gives same-process writes an immediate path to the browser with plain HTTP, while the version-counter poll stays as a fallback that works in every deployment environment, including serverless and edge, where persistent sockets may not be available.
374
+
373
375
  ```ts
374
376
  // Client: subscribe to agent/UI data changes once near the app shell
375
377
  import { useDbSync } from "@agent-native/core/client/hooks";
@@ -25,7 +25,9 @@ agent-native upgrade
25
25
  agent-native template sync
26
26
  ```
27
27
 
28
- `sync` defaults to the template version matching your installed `@agent-native/core`, so running it after `upgrade` keeps code and packages on the same version.
28
+ `sync` defaults to the template version matching your installed `@agent-native/core`, so running it after `upgrade` keeps code and packages on the same version. Running the bare `agent-native upgrade` bumps `@agent-native/*` dependencies to `latest`, installs, refreshes scaffold skills (`skills update scaffold --project`), and runs typecheck when available. Use `agent-native upgrade check` first for a doctor-only report of framework overrides/patches and pending bumps.
29
+
30
+ Do **not** fix a broken upgrade with `pnpm.overrides`, `patchedDependencies`, or edits under `node_modules/@agent-native/*` (especially core/dispatch); those mask the real incompatibility instead of resolving it. If upgrade or typecheck fails, fix app-level code or ask, then re-run upgrade.
29
31
 
30
32
  Look before you merge:
31
33
 
@@ -108,4 +108,8 @@ together.
108
108
  See how the same model supports apps led by a UI, chat, automation, or another
109
109
  agent.
110
110
 
111
+ ### [Templates](/docs/cloneable-saas)
112
+
113
+ Start from a complete SaaS product instead of building up from Chat.
114
+
111
115
  </Cards>
@@ -102,6 +102,14 @@ data and the same actions.
102
102
 
103
103
  Keep one canonical instructions file: `AGENTS.md`. If a client expects `CLAUDE.md`, make it a symlink to `AGENTS.md` rather than a second copy. Two hand-maintained files drift, and the agent ends up with contradictory rules. One source of truth, linked where needed.
104
104
 
105
+ `AGENTS.md` is the universal standard the framework builds on, so any AI coding tool that reads project instructions works out of the box:
106
+
107
+ - **Claude Code**: reads `CLAUDE.md` (symlinked from `AGENTS.md` by the CLI setup)
108
+ - **Cursor**: reads `AGENTS.md` directly, or `.cursorrules` (Cursor's legacy location) if present
109
+ - **Windsurf**: reads `AGENTS.md` directly, or `.windsurfrules` (Windsurf's legacy location) if present
110
+ - **Codex, Gemini, and others**: work via the embedded agent panel
111
+ - **Builder.io**: cloud-hosted agent with visual editing and collaboration
112
+
105
113
  ## SKILL.md frontmatter must say what AND when {#skill-frontmatter}
106
114
 
107
115
  The `description` is the only thing the agent sees when deciding whether to read a skill. It must answer two questions: what the skill covers, and when to trigger it. A description that only describes the topic will not fire.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.174.1",
3
+ "version": "0.174.2-nightly-20260825214843",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,257 +0,0 @@
1
- ---
2
- title: "FAQ"
3
- description: "Common questions about Agent-Native — what it is, who it's for, what you can build, and how it works."
4
- ---
5
-
6
- # FAQ
7
-
8
- Common questions about Agent-Native, organized from "I'm just looking" to "I'm wiring up auth right now."
9
-
10
- ## The basics {#general}
11
-
12
- ### What is Agent-Native? {#what-is-agent-native}
13
-
14
- Agent-Native is a framework for building apps where the AI agent and the product surface around it are equal partners. The usual path starts with chat, adds typed actions, renders structured results inline, and grows into durable pages around the same SQL state. The invariant is that agents and humans share the same actions, database, and state. See [What is Agent-Native?](/docs/what-is-agent-native) for the full explanation.
15
-
16
- ### Who is this for? {#who-is-this-for}
17
-
18
- Agent-Native is for people who want a real app and an AI agent to work from the same data and actions. The common paths are:
19
-
20
- - **Use a hosted app** if you want Mail, Calendar, Forms, Plan, or another finished template with no setup — start at the [template gallery](/templates).
21
- - **Start with Chat** if you want the default from-scratch path: users can talk to the agent immediately, then you extend with actions, native results, and screens — start with [Getting Started](/docs/getting-started) or [Chat](/docs/template-chat).
22
- - **Start automation-first** if you are building scheduled jobs, queues, scripts, integration workers, or external-agent workflows with no browser UI yet — start with [Automation-First Apps](/docs/pure-agent-apps).
23
- - **Start from a template and customize it** if you want your own SaaS product with auth, database, UI, and agent actions already wired — see [Templates](/docs/cloneable-saas).
24
- - **Build from scratch** if you want the framework primitives for a new agent-driven product — start with [Getting Started](/docs/getting-started).
25
- - **Connect another agent or code tool** if you want Claude, ChatGPT, Codex, Cursor, or GitHub Copilot / VS Code to use an Agent-Native app — see [External Agents](/docs/external-agents) and [Skills Guide](/docs/skills-guide).
26
-
27
- ### How is this different from adding AI to an existing app? {#how-is-this-different}
28
-
29
- Most apps bolt AI on as an afterthought that can't actually _do_ things in the app. In an Agent-Native app the agent is a first-class citizen that shares the same actions, database, and state as the UI, so it can do anything the buttons can. See [Why build apps this way](/docs/what-is-agent-native#why-build-apps-this-way).
30
-
31
- <Diagram id="doc-block-1s6b04" title="Bolted-on AI vs. Agent-Native" summary="A bolted-on chat sidebar lives in its own world. An Agent-Native agent shares the same actions, database, and state as the UI.">
32
-
33
- ```html
34
- <div class="diagram-vs">
35
- <div class="diagram-col">
36
- <span class="diagram-pill warn">Bolted-on AI</span>
37
- <div class="diagram-node">Chat sidebar</div>
38
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
39
- <div class="diagram-box" data-rough>
40
- separate AI world<br /><small class="diagram-muted"
41
- >can't touch the app</small
42
- >
43
- </div>
44
- <div class="diagram-box diagram-muted">App UI &amp; data</div>
45
- </div>
46
- <div class="diagram-divider" aria-hidden="true"></div>
47
- <div class="diagram-col">
48
- <span class="diagram-pill ok">Agent-Native</span>
49
- <div class="diagram-row2">
50
- <div class="diagram-node">UI</div>
51
- <div class="diagram-node">Agent</div>
52
- </div>
53
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
54
- <div class="diagram-box" data-rough>shared actions, DB &amp; state</div>
55
- </div>
56
- </div>
57
- ```
58
-
59
- ```css
60
- .diagram-vs {
61
- display: flex;
62
- align-items: stretch;
63
- gap: 18px;
64
- flex-wrap: wrap;
65
- }
66
- .diagram-vs .diagram-col {
67
- display: flex;
68
- flex-direction: column;
69
- gap: 8px;
70
- align-items: center;
71
- flex: 1;
72
- min-width: 200px;
73
- }
74
- .diagram-vs .diagram-row2 {
75
- display: flex;
76
- gap: 8px;
77
- }
78
- .diagram-vs .diagram-arrow {
79
- font-size: 20px;
80
- line-height: 1;
81
- }
82
- .diagram-vs .diagram-divider {
83
- width: 1px;
84
- align-self: stretch;
85
- background: currentColor;
86
- opacity: 0.15;
87
- }
88
- ```
89
-
90
- </Diagram>
91
-
92
- ### Is it open source? {#is-this-open-source}
93
-
94
- Yes. The framework and all templates are open source. You can run everything locally, self-host, or use Builder.io's cloud for managed hosting, collaboration, and team features.
95
-
96
- ### How much does it cost? {#how-much}
97
-
98
- The framework itself is free. The two costs you'll see in practice:
99
-
100
- - **AI usage.** You bring your own API key (Anthropic, OpenAI, etc.) and pay the model provider directly. There's no markup from us.
101
- - **Hosting.** Whatever your host charges. Most templates run fine on free tiers (Netlify, Vercel, Cloudflare) for small workloads.
102
-
103
- If you'd rather not manage any of this, the hosted version on `agent-native.com` (operated by Builder.io) bundles inference and hosting into a per-seat plan.
104
-
105
- ### Can I host this myself? {#can-i-self-host}
106
-
107
- Yes. Pick any host that runs Node — Netlify, Vercel, Cloudflare, AWS, Deno Deploy, your own server — and any SQL database (Postgres, SQLite, Turso, D1). The framework is built to be portable. Start with the [local Docker quickstart](/docs/deployment#self-host-quickstart), then use [Deployment](/docs/deployment) for production.
108
-
109
- ### What AI models does it support? {#what-models}
110
-
111
- Anthropic Claude, OpenAI (GPT-5 family), Google Gemini, and any provider that speaks the OpenAI API shape. For OpenAI-compatible gateways like LiteLLM, choose OpenAI in Settings and add the gateway URL under Advanced. You configure the model in settings; switching is a config change, not a code rewrite. The framework's heaviest tested path is Claude, so that's the default recommendation.
112
-
113
- ### Do I need to know AI/ML? {#do-i-need-to-know-ai}
114
-
115
- No. You don't train models, fine-tune, or deal with embeddings. You build a regular web app — and on the hosted version, you barely build anything at all. The framework handles the agent integration: routing messages, running actions, syncing state.
116
-
117
- ### Can I migrate an existing app to Agent-Native? {#can-i-use-existing-code}
118
-
119
- You can, but Agent-Native works best when built from the ground up. The architecture — shared database, polling sync, actions, application state — needs to be integrated throughout. Starting from an app and customizing it is the recommended path. Think of it like the shift from desktop-first to mobile-first: you _can_ retrofit, but building native is better.
120
-
121
- ## Templates and what you can build {#templates}
122
-
123
- ### What templates are available? {#what-templates-are-available}
124
-
125
- The framework ships with production-ready templates including [Chat](/docs/template-chat), [Mail](/docs/template-mail), [Calendar](/docs/template-calendar), [Forms](/docs/template-forms), [Plan](/docs/template-plan) (visual plans and PR recaps), [Analytics](/docs/template-analytics), [Dispatch](/docs/template-dispatch), and more. Each is a complete app with UI, agent actions, database schema, and AI instructions ready to go. See [Templates](/docs/cloneable-saas) for the full catalog.
126
-
127
- ### Can I customize templates? {#can-i-customize-templates}
128
-
129
- That's the whole point. Start from a template and customize it in your own repository and development workflow. "Add a priority field to forms." "Connect to our Salesforce instance." "Change the color scheme to match our brand." An agent can edit the source when its frame is intentionally granted workspace and write tooling; otherwise the embedded agent operates the resulting app through its actions and data surface.
130
-
131
- ### Can I build something the templates don't cover? {#build-from-scratch}
132
-
133
- Yes. If you want a basic agentic app, run `npx @agent-native/core@latest create my-chat-app --template chat`; you get durable chat threads, actions, auth, SQL-backed runtime state, native inline result rendering, and room to add your own screens. If you are building a no-browser automation, run `npx @agent-native/core@latest create my-agent --headless`. See [Getting Started](/docs/getting-started), [Automation-First Apps](/docs/pure-agent-apps), and [Chat](/docs/template-chat).
134
-
135
- ### Can I try it without starting from a template? {#try-with-a-skill}
136
-
137
- Yes — install a skill into a coding agent you already use with one command and no scaffold required. See the [Skills Guide](/docs/skills-guide#app-backed-skills) for the walkthrough.
138
-
139
- ## Agent capabilities {#agent-capabilities}
140
-
141
- ### Does the embedded agent edit the app's source code? {#can-the-agent-modify-code}
142
-
143
- Not by default. The embedded agent normally works through actions, SQL-backed state, and configured integrations. It can edit components, routes, styles, or actions only when its frame is intentionally granted repository workspace and write tooling. Otherwise use your normal development workflow or a separate code-capable development/Builder frame. For runtime customization without source changes, use [Extensions](/docs/extensions).
144
-
145
- ### Can users talk to the agent from outside the app? {#external-channels}
146
-
147
- Yes. The same agent runs in your web UI, in Slack, in Telegram, over email, and from other agents (via [A2A](/docs/a2a-protocol)). It's the same agent with the same memory and the same actions, just reached through different channels. See [Messaging the agent](/docs/messaging).
148
-
149
- ### Can agents talk to each other? {#can-agents-talk-to-each-other}
150
-
151
- Yes, via the [A2A (Agent-to-Agent) protocol](/docs/a2a-protocol). Every Agent-Native app automatically gets an A2A endpoint. From the mail app, you can tag the analytics agent to query data. An agent discovers what other agents are available, calls them over the protocol, and shows results in the UI. No configuration needed — the agent card is auto-generated from your template's actions.
152
-
153
- ### What can the agent see in the app? {#what-can-the-agent-see}
154
-
155
- The agent always knows what the user is currently viewing. The UI writes navigation state to the database on every route change — which view is open, which item is selected. The agent reads this before taking action. If an email is open, the agent knows which email. If a slide is selected, the agent knows which slide. See [Context Awareness](/docs/context-awareness).
156
-
157
- ## Development questions {#development}
158
-
159
- ### Which AI coding tools work with Agent-Native? {#which-ai-tools-work}
160
-
161
- Any AI coding tool that reads project instructions. The framework uses AGENTS.md as the universal standard:
162
-
163
- - **Claude Code** — reads CLAUDE.md (symlinked from AGENTS.md by the CLI setup)
164
- - **Cursor** — reads AGENTS.md directly, or `.cursorrules` (Cursor's legacy location) if present in your project
165
- - **Windsurf** — reads AGENTS.md directly, or `.windsurfrules` (Windsurf's legacy location) if present in your project
166
- - **Codex, Gemini, and others** — work via the embedded agent panel
167
- - **Builder.io** — cloud-hosted agent with visual editing and collaboration
168
-
169
- ### Can I use my own database? {#can-i-use-my-own-database}
170
-
171
- Yes. Set `DATABASE_URL` and the framework auto-detects it. Supported databases include SQLite, Postgres (Neon, Supabase, plain), Turso (libSQL), and Cloudflare D1. All SQL is dialect-agnostic via Drizzle ORM — the same code works everywhere.
172
-
173
- ### Where can I deploy? {#where-can-i-deploy}
174
-
175
- Anywhere. The server runs on Nitro, which compiles to any deployment target: Node.js, Cloudflare Workers/Pages, Netlify, Vercel, Deno Deploy, AWS Lambda, and Bun. You can also use Builder.io's hosting for managed deployments. See the [Deployment guide](/docs/deployment).
176
-
177
- ## Architecture {#architecture}
178
-
179
- ### Why SSE plus polling instead of WebSockets? {#why-polling-not-websockets}
180
-
181
- SSE gives same-process writes an immediate path to the browser, and a lightweight version-counter poll remains the fallback because it works in every deployment environment — including serverless and edge, where persistent sockets may not be available. See [Key Concepts — Live sync](/docs/key-concepts#polling-sync).
182
-
183
- <Diagram id="doc-block-1et8179" title="SSE first, polling fallback" summary={"Same-process writes stream instantly; a version-counter poll keeps serverless, edge, and cross-process writes convergent."}>
184
-
185
- ```html
186
- <div class="diagram-transport">
187
- <div class="diagram-box" data-rough>DB write</div>
188
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
189
- <div class="diagram-col">
190
- <div class="diagram-node">
191
- SSE<br /><small class="diagram-muted"
192
- >/_agent-native/events &middot; instant</small
193
- >
194
- </div>
195
- <div class="diagram-node">
196
- Poll<br /><small class="diagram-muted"
197
- >/_agent-native/poll &middot; universal fallback</small
198
- >
199
- </div>
200
- </div>
201
- <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
202
- <div class="diagram-box" data-rough>Browser refetch</div>
203
- </div>
204
- ```
205
-
206
- ```css
207
- .diagram-transport {
208
- display: flex;
209
- align-items: center;
210
- gap: 12px;
211
- flex-wrap: wrap;
212
- }
213
- .diagram-transport .diagram-col {
214
- display: flex;
215
- flex-direction: column;
216
- gap: 8px;
217
- }
218
- .diagram-transport .diagram-arrow {
219
- font-size: 22px;
220
- line-height: 1;
221
- }
222
- ```
223
-
224
- </Diagram>
225
-
226
- ### Why can't the UI call an LLM directly? {#why-no-inline-llm-calls}
227
-
228
- AI is non-deterministic, so you need conversation flow to give feedback and iterate — not one-shot buttons — and the agent already has your codebase, instructions, skills, and history that an inline call lacks. Routing everything through the agent is also what lets the app be driven from Slack, Telegram, or another agent. See [Key Concepts — Agent chat bridge](/docs/key-concepts#agent-chat-bridge).
229
-
230
- ### How do I upgrade an older Agent-Native app? {#upgrade-older-app}
231
-
232
- Run the supported upgrade path from the app or workspace root:
233
-
234
- ```bash
235
- npx @agent-native/core@latest upgrade
236
- ```
237
-
238
- That bumps `@agent-native/*` dependencies to `latest`, installs, refreshes
239
- scaffold skills (`skills update scaffold --project`), and runs typecheck when
240
- available. Use `upgrade check` first if you want a doctor-only report of
241
- framework overrides/patches and pending bumps.
242
-
243
- Do **not** fix a broken upgrade with `pnpm.overrides`, `patchedDependencies`,
244
- or edits under `node_modules/@agent-native/*` (especially core/dispatch). If
245
- upgrade or typecheck fails, fix app-level code or ask — then re-run upgrade.
246
-
247
- ### Why is this a framework and not a library? {#why-framework-not-library}
248
-
249
- The shared database, live sync, actions system, and application state only work because they're wired together from the ground up — the UI reacts to agent changes instantly, agents communicate, and the agent understands what the user is looking at. A library gives you pieces; this is an architecture. See [Key Concepts](/docs/key-concepts).
250
-
251
- ## What's next
252
-
253
- - [**Getting Started**](/docs/getting-started) — build your first action and render it inline in chat
254
- - [**What is Agent-Native?**](/docs/what-is-agent-native) — the full explanation behind the answers above
255
- - [**Templates**](/docs/cloneable-saas) — start from a complete SaaS product instead of starting from scratch
256
- - [**Key Concepts**](/docs/key-concepts) — the architecture: SQL, actions, polling sync, and portability
257
- - [**Deployment**](/docs/deployment) — host on your own domain or any Nitro-compatible provider