@agent-native/core 0.63.1 → 0.63.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/agent/harness/ai-sdk-adapter.d.ts +44 -0
  2. package/dist/agent/harness/ai-sdk-adapter.d.ts.map +1 -1
  3. package/dist/agent/harness/ai-sdk-adapter.js +120 -1
  4. package/dist/agent/harness/ai-sdk-adapter.js.map +1 -1
  5. package/dist/agent/harness/index.d.ts +1 -1
  6. package/dist/agent/harness/index.d.ts.map +1 -1
  7. package/dist/agent/harness/index.js.map +1 -1
  8. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  9. package/dist/client/blocks/library/AnnotatedCodeBlock.js +29 -10
  10. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  11. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  12. package/dist/client/blocks/library/DiffBlock.js +48 -20
  13. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  14. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  15. package/dist/client/blocks/library/diagram.js +14 -3
  16. package/dist/client/blocks/library/diagram.js.map +1 -1
  17. package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
  18. package/dist/client/blocks/library/wireframe.js +14 -3
  19. package/dist/client/blocks/library/wireframe.js.map +1 -1
  20. package/dist/client/blocks/types.d.ts +5 -0
  21. package/dist/client/blocks/types.d.ts.map +1 -1
  22. package/dist/client/blocks/types.js.map +1 -1
  23. package/dist/server/action-discovery.d.ts.map +1 -1
  24. package/dist/server/action-discovery.js +24 -2
  25. package/dist/server/action-discovery.js.map +1 -1
  26. package/dist/server/deep-link.d.ts +2 -2
  27. package/dist/server/deep-link.d.ts.map +1 -1
  28. package/dist/server/deep-link.js +2 -2
  29. package/dist/server/deep-link.js.map +1 -1
  30. package/dist/styles/blocks.css +25 -3
  31. package/dist/tailwind.preset.d.ts.map +1 -1
  32. package/dist/tailwind.preset.js +8 -1
  33. package/dist/tailwind.preset.js.map +1 -1
  34. package/dist/templates/default/package.json +1 -0
  35. package/dist/templates/headless/AGENTS.md +3 -0
  36. package/dist/templates/headless/DEVELOPING.md +4 -0
  37. package/dist/templates/headless/actions/run.ts +6 -0
  38. package/docs/content/a2a-protocol.md +48 -10
  39. package/docs/content/actions.md +35 -16
  40. package/docs/content/agent-mentions.md +25 -32
  41. package/docs/content/agent-surfaces.md +31 -0
  42. package/docs/content/agent-teams.md +17 -14
  43. package/docs/content/agent-web-surfaces.md +24 -15
  44. package/docs/content/authentication.md +21 -0
  45. package/docs/content/automations.md +37 -21
  46. package/docs/content/blueprint-installer.md +7 -0
  47. package/docs/content/cli-adapters.md +7 -0
  48. package/docs/content/client.md +14 -0
  49. package/docs/content/cloneable-saas.md +14 -0
  50. package/docs/content/code-agents-ui.md +30 -2
  51. package/docs/content/components.md +21 -0
  52. package/docs/content/context-awareness.md +33 -48
  53. package/docs/content/creating-templates.md +43 -52
  54. package/docs/content/cross-app-sso.md +41 -0
  55. package/docs/content/database.md +41 -21
  56. package/docs/content/deployment.md +23 -6
  57. package/docs/content/dispatch.md +27 -0
  58. package/docs/content/drop-in-agent.md +26 -27
  59. package/docs/content/durable-resume.md +13 -1
  60. package/docs/content/embedding-sdk.md +14 -0
  61. package/docs/content/evals.md +14 -0
  62. package/docs/content/extensions.md +19 -0
  63. package/docs/content/external-agents.md +39 -2
  64. package/docs/content/faq.md +14 -0
  65. package/docs/content/file-uploads.md +20 -0
  66. package/docs/content/frames.md +14 -0
  67. package/docs/content/getting-started.md +34 -16
  68. package/docs/content/harness-agents.md +49 -8
  69. package/docs/content/human-approval.md +15 -30
  70. package/docs/content/key-concepts.md +37 -0
  71. package/docs/content/local-file-mode.md +26 -19
  72. package/docs/content/mcp-apps.md +36 -1
  73. package/docs/content/mcp-clients.md +49 -0
  74. package/docs/content/mcp-protocol.md +36 -0
  75. package/docs/content/messaging.md +34 -8
  76. package/docs/content/migration-workbench.md +7 -0
  77. package/docs/content/multi-app-workspace.md +29 -16
  78. package/docs/content/multi-tenancy.md +14 -0
  79. package/docs/content/native-chat-ui.md +20 -3
  80. package/docs/content/notifications.md +30 -0
  81. package/docs/content/observability.md +20 -1
  82. package/docs/content/observational-memory.md +14 -0
  83. package/docs/content/onboarding.md +32 -41
  84. package/docs/content/plan-plugin.md +14 -0
  85. package/docs/content/pr-visual-recap.md +14 -0
  86. package/docs/content/processors.md +7 -0
  87. package/docs/content/progress.md +23 -0
  88. package/docs/content/pure-agent-apps.md +7 -0
  89. package/docs/content/real-time-collaboration.md +19 -18
  90. package/docs/content/recurring-jobs.md +22 -19
  91. package/docs/content/routing.md +8 -0
  92. package/docs/content/sandbox-adapters.md +19 -0
  93. package/docs/content/security.md +38 -0
  94. package/docs/content/server.md +47 -25
  95. package/docs/content/sharing.md +50 -0
  96. package/docs/content/skills-guide.md +27 -42
  97. package/docs/content/template-analytics.md +60 -0
  98. package/docs/content/template-assets.md +68 -0
  99. package/docs/content/template-brain.md +83 -0
  100. package/docs/content/template-calendar.md +74 -0
  101. package/docs/content/template-chat.md +19 -0
  102. package/docs/content/template-clips.md +113 -0
  103. package/docs/content/template-content.md +133 -0
  104. package/docs/content/template-design.md +55 -0
  105. package/docs/content/template-dispatch.md +50 -0
  106. package/docs/content/template-forms.md +59 -0
  107. package/docs/content/template-mail.md +62 -0
  108. package/docs/content/template-plan.md +80 -5
  109. package/docs/content/template-slides.md +82 -0
  110. package/docs/content/template-videos.md +62 -0
  111. package/docs/content/tracking.md +21 -0
  112. package/docs/content/using-your-agent.md +14 -0
  113. package/docs/content/voice-input.md +31 -10
  114. package/docs/content/what-is-agent-native.md +25 -13
  115. package/docs/content/workspace-connections.md +49 -0
  116. package/docs/content/workspace-management.md +24 -0
  117. package/docs/content/workspace.md +50 -19
  118. package/docs/content/writing-agent-instructions.md +7 -0
  119. package/package.json +6 -2
  120. package/src/templates/default/package.json +1 -0
  121. package/src/templates/headless/AGENTS.md +3 -0
  122. package/src/templates/headless/DEVELOPING.md +4 -0
  123. package/src/templates/headless/actions/run.ts +6 -0
@@ -25,6 +25,13 @@ There are three layers:
25
25
  - **Desktop**: the left-sidebar Code tab adds native terminal launch, app webviews, and desktop deep links while using the same run model.
26
26
  - **Shared UI**: `@agent-native/code-agents-ui` renders the reusable React surface.
27
27
 
28
+ ```an-diagram title="Three layers over one run store" summary="CLI, Desktop, and the shared UI are different surfaces over the same file-backed run store and executor; hosts adapt it via the CodeAgentsHost contract."
29
+ {
30
+ "html": "<div class=\"diagram-layers\"><div class=\"diagram-row\"><div class=\"diagram-card\" data-rough><span class=\"diagram-pill\">CLI</span><small class=\"diagram-muted\">start · resume · status · stop</small></div><div class=\"diagram-card\" data-rough><span class=\"diagram-pill\">Desktop</span><small class=\"diagram-muted\">native terminal · webviews · deep links</small></div><div class=\"diagram-card\" data-rough><span class=\"diagram-pill accent\">Shared UI</span><small class=\"diagram-muted\">@agent-native/code-agents-ui</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill\">CodeAgentsHost</span><small class=\"diagram-muted\">host contract</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-box\" data-rough>File-backed run store + executor<br><small class=\"diagram-muted\">@agent-native/core/code-agents</small></div></div>",
31
+ "css": ".diagram-layers{display:flex;flex-direction:column;gap:10px;align-items:center}.diagram-layers .diagram-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}.diagram-layers .diagram-card{display:flex;flex-direction:column;gap:4px;padding:12px 16px}.diagram-layers .diagram-arrow{font-size:22px;line-height:1}.diagram-layers .center{display:flex;flex-direction:column;align-items:center;gap:4px}"
32
+ }
33
+ ```
34
+
28
35
  The current split is intentionally converging: the standard agent sidebar and Agent Teams run on the core `run-manager` lifecycle, while Agent-Native Code uses local long-running sessions backed by the file-based Code run store and the shared background-run controller vocabulary.
29
36
 
30
37
  The shared UI is host-driven. It does not know whether it is running in Electron, a browser template, or a future hosted shell. Hosts provide a `CodeAgentsHost` implementation.
@@ -89,8 +96,9 @@ codex login status
89
96
  Desktop and the CLI read `codex login status` and run `codex exec`, so they
90
97
  reuse whatever ChatGPT subscription or API-key auth your installed Codex CLI
91
98
  reports. This is separate from the `@ai-sdk/harness-codex` package used by
92
- [Harness Agents](/docs/harness-agents); the harness adapter does not add a
93
- separate Agent-Native OAuth flow.
99
+ [Harness Agents](/docs/harness-agents); the harness adapter can copy local
100
+ Codex CLI auth into a trusted sandbox only when `codexCliAuth: true` is
101
+ explicitly enabled.
94
102
 
95
103
  ## Browser Host
96
104
 
@@ -365,8 +373,28 @@ The connection is outbound-only from Desktop:
365
373
  5. Mobile reads `hosts`, `runs`, and `transcript` from Dispatch; it never talks
366
374
  directly to the desktop.
367
375
 
376
+ ```an-diagram title="Remote Dispatch is outbound-only" summary="Mobile never talks to the desktop directly. Desktop long-polls Dispatch, claims commands, drives the local run store, and mirrors results back."
377
+ {
378
+ "html": "<div class=\"diagram-remote\"><div class=\"diagram-node\" data-rough>Mobile / Telegram<br><small class=\"diagram-muted\">/code · sessions</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>Dispatch relay<br><small class=\"diagram-muted\">hosts · runs · transcript</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&larr;</div><div class=\"diagram-node\" data-rough>Desktop<br><small class=\"diagram-muted\">long-polls · claims · drives run store</small></div></div>",
379
+ "css": ".diagram-remote{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-remote .diagram-arrow{font-size:22px;line-height:1}"
380
+ }
381
+ ```
382
+
368
383
  The canonical remote relay endpoints are:
369
384
 
385
+ ```an-api title="Desktop claims queued work"
386
+ {
387
+ "method": "POST",
388
+ "path": "/_agent-native/integrations/remote/poll",
389
+ "summary": "Desktop long-polls the relay to claim enqueued commands",
390
+ "description": "Outbound-only from a paired Desktop host. Desktop authenticates with its device token and claims work that mobile or Telegram enqueued.",
391
+ "auth": "Desktop device token",
392
+ "responses": [
393
+ { "status": "200", "description": "Claimed commands for this host (may be empty after the long-poll window)." }
394
+ ]
395
+ }
396
+ ```
397
+
370
398
  | Method | Route | Caller | Purpose |
371
399
  | ---------- | -------------------------------------------------------- | --------------- | ------------------------------------------- |
372
400
  | `POST` | `/_agent-native/integrations/remote/register` | Desktop session | Pair a desktop host and return a token once |
@@ -25,6 +25,13 @@ Avoid importing UI components from the bare `@agent-native/core` package. Use
25
25
  `@agent-native/core/client` or a focused `@agent-native/core/client/*` subpath
26
26
  so bundlers choose the browser-safe entry.
27
27
 
28
+ ```an-diagram title="Drop down a layer, not out of the framework" summary="Each layer keeps the same runtime — actions, thread state, and SQL-backed sync — while giving you more control over the chrome."
29
+ {
30
+ "html": "<div class=\"diagram-layers\"><div class=\"diagram-card layer\"><span class=\"diagram-pill accent\">&lt;AgentSidebar&gt;</span><small class=\"diagram-muted\">Whole sidebar around your app. The 80% case.</small></div><div class=\"diagram-card layer l2\"><span class=\"diagram-pill\">&lt;AgentPanel&gt; &middot; &lt;AgentChatSurface&gt;</span><small class=\"diagram-muted\">The panel or a chat page in your own layout.</small></div><div class=\"diagram-card layer l3\"><span class=\"diagram-pill\">&lt;AssistantChat&gt; + runtime</span><small class=\"diagram-muted\">Own the chrome; optionally pass a BYO AgentChatRuntime.</small></div><div class=\"diagram-card layer l4\"><span class=\"diagram-pill\">&lt;PromptComposer&gt; &middot; &lt;AgentConversation&gt;</span><small class=\"diagram-muted\">Composer and transcript primitives only.</small></div><div class=\"diagram-rail\" data-rough>Same runtime: actions &middot; thread state &middot; SQL-backed sync</div></div>",
31
+ "css": ".diagram-layers{display:flex;flex-direction:column;gap:10px}.diagram-layers .layer{display:flex;flex-direction:column;gap:4px;padding:12px 14px}.diagram-layers .l2{margin-inline-start:24px}.diagram-layers .l3{margin-inline-start:48px}.diagram-layers .l4{margin-inline-start:72px}.diagram-layers .diagram-rail{margin-top:6px;padding:10px 14px;text-align:center}"
32
+ }
33
+ ```
34
+
28
35
  ## Agent And Chat UI {#agent-chat-ui}
29
36
 
30
37
  | API | Import path | Use when |
@@ -171,6 +178,13 @@ collaborative document hooks.
171
178
  | `useCollaborativeMap()` / `useCollaborativeArray()` | Experiment with structured Y.Map/Y.Array state when rich-text body collab is the wrong fit. |
172
179
  | `dedupeCollabUsersByEmail()` | Build a custom avatar stack without duplicate tabs for the same user. |
173
180
 
181
+ ```an-diagram title="Presence: humans and the agent share one awareness layer" summary="useCollaborativeDoc owns the awareness instance; client hooks publish cursors and selections; server helpers let an agent action appear as a live participant."
182
+ {
183
+ "html": "<div class=\"diagram-presence\"><div class=\"diagram-col\"><div class=\"diagram-node\">Humans<br><small class=\"diagram-muted\">usePresence &middot; cursors, selection</small></div><div class=\"diagram-node diagram-accent\">Agent action<br><small class=\"diagram-muted\">agentUpdateSelection()</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">useCollaborativeDoc</span><small class=\"diagram-muted\">awareness layer</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\">&lt;PresenceBar&gt; &middot; &lt;LiveCursorOverlay&gt;<br><small class=\"diagram-muted\">render everyone, agent included</small></div></div>",
184
+ "css": ".diagram-presence{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-presence .diagram-col{display:flex;flex-direction:column;gap:10px}.diagram-presence .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px}.diagram-presence .diagram-arrow{font-size:22px;line-height:1}"
185
+ }
186
+ ```
187
+
174
188
  Server-side agent actions that want to appear as a live participant use the
175
189
  lower-level `@agent-native/core/collab` agent presence helpers:
176
190
 
@@ -324,6 +338,13 @@ If you truly need raw text-in/text-out, keep it server-side and use
324
338
  `completeText()` from `@agent-native/core/server`. Wrap user-facing usage in an
325
339
  action so the UI and agent share the same capability.
326
340
 
341
+ ```an-callout
342
+ {
343
+ "tone": "warning",
344
+ "body": "`completeText()` is the escape hatch, not the default. Reach for it only for true text-in/text-out (a label, a one-line rewrite). Anything needing tools, state, auditability, or steering belongs in an action plus `sendToAgentChat({ background: true })`."
345
+ }
346
+ ```
347
+
327
348
  ```ts
328
349
  import { defineAction } from "@agent-native/core/action";
329
350
  import { completeText } from "@agent-native/core/server";
@@ -24,6 +24,13 @@ Six patterns solve this:
24
24
  5. **Prompt handoff** -- UI controls call `sendToAgentChat()` when a click should become an agent turn
25
25
  6. **`navigate`** -- a one-shot command from the agent that tells the UI where to go
26
26
 
27
+ ```an-diagram title="How the agent sees what you see" summary="The UI writes lightweight state keys; view-screen hydrates them into real records; the agent can write navigate back to move the UI."
28
+ {
29
+ "html": "<div class=\"diagram-ctx\"><div class=\"diagram-card col\"><span class=\"diagram-pill\">UI writes</span><div class=\"diagram-node\">navigation<br><small class=\"diagram-muted\">view, open ids</small></div><div class=\"diagram-node\">__url__<br><small class=\"diagram-muted\">shareable filters</small></div><div class=\"diagram-node\">selection<br><small class=\"diagram-muted\">rows, blocks, shapes</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">view-screen</span><small class=\"diagram-muted\">reads state &middot; fetches records</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\">Agent acts<br><small class=\"diagram-muted\">on the real object</small></div><div class=\"diagram-arrow diagram-accent\" aria-hidden=\"true\">&#8635;</div><div class=\"diagram-box diagram-accent\">navigate<br><small class=\"diagram-muted\">agent moves the UI</small></div></div>",
30
+ "css": ".diagram-ctx{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-ctx .col{display:flex;flex-direction:column;gap:8px;padding:14px}.diagram-ctx .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px}.diagram-ctx .diagram-arrow{font-size:22px;line-height:1}"
31
+ }
32
+ ```
33
+
27
34
  ## Context layers {#context-layers}
28
35
 
29
36
  Use different context channels for different jobs:
@@ -184,58 +191,29 @@ Use `pending-selection-context` for one-shot "act on this exact highlighted text
184
191
 
185
192
  Every template should have a `view-screen` action. It reads navigation and selection state, fetches the relevant data, and returns a snapshot of what the user sees. This is the agent's eyes.
186
193
 
187
- ```ts
188
- // actions/view-screen.ts
189
- import { defineAction } from "@agent-native/core/action";
190
- import { readAppState } from "@agent-native/core/application-state";
191
- import { eq, inArray } from "drizzle-orm";
192
- import { z } from "zod";
193
- import { getDb, schema } from "../server/db/index.js";
194
-
195
- export default defineAction({
196
- description:
197
- "See what the user is currently looking at on screen. Reads navigation and selection state and fetches matching data.",
198
- schema: z.object({}),
199
- http: false,
200
- run: async () => {
201
- const navigation = (await readAppState("navigation")) as any;
202
- const selection = (await readAppState("selection")) as any;
203
- const screen: Record<string, unknown> = {};
204
- if (navigation) screen.navigation = navigation;
205
- if (selection) screen.selection = selection;
206
-
207
- const db = getDb();
208
-
209
- // Fetch data based on what the user is viewing
210
- if (navigation?.view === "inbox") {
211
- screen.emailList = await db
212
- .select()
213
- .from(schema.emails)
214
- .where(eq(schema.emails.label, navigation.label));
215
- }
216
- if (navigation?.threadId) {
217
- screen.thread = await db
218
- .select()
219
- .from(schema.threads)
220
- .where(eq(schema.threads.id, navigation.threadId));
221
- }
222
- if (selection?.kind === "email.messages") {
223
- screen.selectedMessages = await db
224
- .select()
225
- .from(schema.emails)
226
- .where(inArray(schema.emails.id, selection.messageIds));
227
- }
228
-
229
- if (Object.keys(screen).length === 0) {
230
- return "No application state found. Is the app running?";
231
- }
232
- return screen;
233
- },
234
- });
194
+ ```an-annotated-code title="view-screen — the agent's eyes"
195
+ {
196
+ "filename": "actions/view-screen.ts",
197
+ "language": "ts",
198
+ "code": "import { defineAction } from \"@agent-native/core/action\";\nimport { readAppState } from \"@agent-native/core/application-state\";\nimport { eq, inArray } from \"drizzle-orm\";\nimport { z } from \"zod\";\nimport { getDb, schema } from \"../server/db/index.js\";\n\nexport default defineAction({\n description:\n \"See what the user is currently looking at on screen.\",\n schema: z.object({}),\n http: false,\n run: async () => {\n const navigation = (await readAppState(\"navigation\")) as any;\n const selection = (await readAppState(\"selection\")) as any;\n const screen: Record<string, unknown> = {};\n if (navigation) screen.navigation = navigation;\n if (selection) screen.selection = selection;\n\n const db = getDb();\n\n // Fetch data based on what the user is viewing\n if (navigation?.view === \"inbox\") {\n screen.emailList = await db\n .select()\n .from(schema.emails)\n .where(eq(schema.emails.label, navigation.label));\n }\n if (navigation?.threadId) {\n screen.thread = await db\n .select()\n .from(schema.threads)\n .where(eq(schema.threads.id, navigation.threadId));\n }\n if (selection?.kind === \"email.messages\") {\n screen.selectedMessages = await db\n .select()\n .from(schema.emails)\n .where(inArray(schema.emails.id, selection.messageIds));\n }\n\n if (Object.keys(screen).length === 0) {\n return \"No application state found. Is the app running?\";\n }\n return screen;\n },\n});",
199
+ "annotations": [
200
+ { "lines": "10-11", "label": "Tool surface", "note": "The agent reads this description to know it can call `view-screen` to see the current UI." },
201
+ { "lines": "13", "label": "http: false", "note": "Internal action — not exposed over HTTP. The agent and `pnpm action` call it, not the browser." },
202
+ { "lines": "15-16", "label": "Read state", "note": "Pulls the lightweight `navigation` and `selection` keys the UI wrote." },
203
+ { "lines": "23-37", "label": "Hydrate", "note": "Turns those IDs into **fresh** records straight from SQL, so the agent verifies the live object before acting." }
204
+ ]
205
+ }
235
206
  ```
236
207
 
237
208
  The agent should call `pnpm action view-screen` before acting on the current UI. This is a hard convention across all templates. When adding new features, update `view-screen` to return data for the new view and any new selection shape.
238
209
 
210
+ ```an-callout
211
+ {
212
+ "tone": "info",
213
+ "body": "**Keep `navigation` and `selection` small.** Store IDs plus short labels, not whole records. `view-screen` fetches the source of truth on demand, so stale or bulky state never reaches the agent."
214
+ }
215
+ ```
216
+
239
217
  ## Prompt handoff with `sendToAgentChat()` {#send-to-agent-chat}
240
218
 
241
219
  Sometimes context should not just sit in app state. A user clicks a button, drops a comment pin, selects an item and chooses "Ask agent", or presses an AI command in a toolbar. That click is an instruction. In browser UI, hand it to the agent with `sendToAgentChat()`.
@@ -386,3 +364,10 @@ How it works:
386
364
  - The server stores the source on each event
387
365
  - When processing sync events, the UI filters out events matching its own `ignoreSource` value -- so it doesn't refetch data it just wrote
388
366
  - Events from agents, other tabs, and actions still come through normally
367
+
368
+ ```an-diagram title="Source tagging stops self-refetch jitter" summary="A tab ignores sync events stamped with its own TAB_ID, but still reacts to agent and other-tab writes."
369
+ {
370
+ "html": "<div class=\"diagram-jitter\"><div class=\"diagram-node\">This tab writes<br><small class=\"diagram-muted\">X-Request-Source: TAB_ID</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>Server stores source<br>on the event</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card col\"><div class=\"diagram-pill warn\">source == TAB_ID &rarr; ignored</div><small class=\"diagram-muted\">no refetch, no flicker</small><div class=\"diagram-pill ok\">agent / other tab &rarr; applied</div><small class=\"diagram-muted\">UI updates live</small></div></div>",
371
+ "css": ".diagram-jitter{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-jitter .col{display:flex;flex-direction:column;gap:6px;padding:14px}.diagram-jitter .diagram-arrow{font-size:22px;line-height:1}"
372
+ }
373
+ ```
@@ -39,37 +39,41 @@ If you are not building a reusable UI template yet, use the headless on-ramp in
39
39
 
40
40
  Every template follows the same broad layout:
41
41
 
42
- ```text
43
- my-template/
44
- app/
45
- root.tsx # HTML shell and providers
46
- routes/ # React Router file routes
47
- components/ # Template UI
48
- hooks/ # UI state and data hooks
49
-
50
- actions/
51
- *.ts # defineAction operations
52
-
53
- server/
54
- db/schema.ts # Drizzle schema
55
- plugins/db.ts # additive migrations
56
- plugins/*.ts # startup integrations
57
- routes/api/*.ts # custom routes only when actions are not enough
58
-
59
- shared/
60
- types.ts # shared client/server types
61
-
62
- .agents/skills/
63
- <skill>/SKILL.md # agent guidance for complex workflows
64
-
65
- AGENTS.md # template-specific agent instructions
66
- package.json
67
- react-router.config.ts
68
- vite.config.ts
42
+ ```an-file-tree title="Template project layout"
43
+ {
44
+ "title": "my-template/",
45
+ "entries": [
46
+ { "path": "app/", "note": "React frontend" },
47
+ { "path": "app/root.tsx", "note": "HTML shell and providers" },
48
+ { "path": "app/routes/", "note": "React Router file routes" },
49
+ { "path": "app/components/", "note": "Template UI" },
50
+ { "path": "app/hooks/", "note": "UI state and data hooks" },
51
+ { "path": "actions/", "note": "defineAction operations — the single source of truth" },
52
+ { "path": "server/db/schema.ts", "note": "Drizzle schema" },
53
+ { "path": "server/plugins/db.ts", "note": "additive migrations" },
54
+ { "path": "server/plugins/", "note": "startup integrations" },
55
+ { "path": "server/routes/api/", "note": "custom routes only when actions are not enough" },
56
+ { "path": "shared/types.ts", "note": "shared client/server types" },
57
+ { "path": ".agents/skills/", "note": "<skill>/SKILL.md agent guidance for complex workflows" },
58
+ { "path": "AGENTS.md", "note": "template-specific agent instructions" },
59
+ { "path": "package.json" },
60
+ { "path": "react-router.config.ts" },
61
+ { "path": "vite.config.ts" }
62
+ ]
63
+ }
69
64
  ```
70
65
 
71
66
  Do not add a `data/` directory for application state. Durable app data belongs in SQL, and the UI reads it through actions or typed server handlers.
72
67
 
68
+ The four areas of every template wire together through one shared action surface and one SQL database — the agent and the UI are equal partners over the same operations:
69
+
70
+ ```an-diagram title="How a template's four areas connect" summary="The UI and the agent both reach SQL through the same actions; application state and polling sync keep them aligned."
71
+ {
72
+ "html": "<div class=\"diagram-tmpl\"><div class=\"diagram-col\"><div class=\"diagram-node\">React UI<br><small class=\"diagram-muted\">app/routes · components</small></div><div class=\"diagram-node\">Agent<br><small class=\"diagram-muted\">AGENTS.md · skills</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\"><span class=\"diagram-pill accent\">Actions</span><small class=\"diagram-muted\">defineAction()</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>SQL via Drizzle<br><small class=\"diagram-muted\">additive schema</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&#8635;</div><div class=\"diagram-pill ok\">Polling sync</div></div>",
73
+ "css": ".diagram-tmpl{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-tmpl .diagram-col{display:flex;flex-direction:column;gap:10px}.diagram-tmpl .diagram-arrow{font-size:22px;line-height:1}.diagram-tmpl .center{display:flex;flex-direction:column;align-items:center;gap:4px}"
74
+ }
75
+ ```
76
+
73
77
  ## Model Data In SQL {#data-models}
74
78
 
75
79
  Define domain tables with the framework Drizzle helpers so schemas stay portable across SQLite, Postgres, D1, Turso, Supabase, Neon, and other supported backends:
@@ -137,31 +141,18 @@ Use the [Database](/docs/database) and [Security](/docs/security) docs before ad
137
141
 
138
142
  Actions are the single source of truth for app behavior. The agent calls them as tools, the frontend calls them through hooks, and other apps can reach them through MCP/A2A.
139
143
 
140
- ```ts
141
- // actions/create-project.ts
142
- import { defineAction } from "@agent-native/core/action";
143
- import { getDb } from "../server/db/index.js"; // getDb is created per app via createGetDb(schema) in server/db/index.ts
144
- import { nanoid } from "nanoid";
145
- import { z } from "zod";
146
- import * as schema from "../server/db/schema";
147
-
148
- export default defineAction({
149
- description: "Create a project.",
150
- schema: z.object({
151
- title: z.string().min(1).describe("Project title"),
152
- }),
153
- run: async ({ title }, ctx) => {
154
- const db = getDb();
155
- const id = nanoid();
156
- await db.insert(schema.projects).values({
157
- id,
158
- title,
159
- ownerEmail: ctx.userEmail,
160
- orgId: ctx.orgId,
161
- });
162
- return { id, title };
163
- },
164
- });
144
+ ```an-annotated-code title="actions/create-project.ts"
145
+ {
146
+ "filename": "actions/create-project.ts",
147
+ "language": "ts",
148
+ "code": "import { defineAction } from \"@agent-native/core/action\";\nimport { getDb } from \"../server/db/index.js\";\nimport { nanoid } from \"nanoid\";\nimport { z } from \"zod\";\nimport * as schema from \"../server/db/schema\";\n\nexport default defineAction({\n description: \"Create a project.\",\n schema: z.object({\n title: z.string().min(1).describe(\"Project title\"),\n }),\n run: async ({ title }, ctx) => {\n const db = getDb();\n const id = nanoid();\n await db.insert(schema.projects).values({\n id,\n title,\n ownerEmail: ctx.userEmail,\n orgId: ctx.orgId,\n });\n return { id, title };\n },\n});",
149
+ "annotations": [
150
+ { "lines": "2", "note": "`getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`." },
151
+ { "lines": "8", "label": "Tool surface", "note": "The `description` is what the agent reads to decide when to call this action as a tool." },
152
+ { "lines": "9-11", "label": "Typed contract", "note": "One zod `schema` validates input from the agent, the UI, HTTP, MCP, and A2A." },
153
+ { "lines": "18-19", "label": "Scoped write", "note": "Stamp `ownerEmail` / `orgId` from `ctx` so the row is correctly scoped for sharing and access checks." }
154
+ ]
155
+ }
165
156
  ```
166
157
 
167
158
  Use `http: { method: "GET" }` or `readOnly: true` for read-only actions. Use `parallelSafe: true` only for mutating actions that are safe to run concurrently with same-turn tool calls. Use `toolCallable: false` for high-blast-radius actions that should not run from sandboxed tools.
@@ -26,6 +26,13 @@ This makes the rollout safe even though it logs people out. **Logout is expected
26
26
 
27
27
  The flow is a standard authorize → signed-token → callback redirect, with email as the only thing that crosses the trust boundary.
28
28
 
29
+ ```an-diagram title="Identity federation flow" summary="Dispatch authenticates the human and returns a short-lived signed assertion of one thing — the verified email. The app links by email and mints its own local session."
30
+ {
31
+ "html": "<div class=\"diagram-sso\"><div class=\"diagram-card\" data-rough><strong>Client app</strong><small class=\"diagram-muted\">own user store</small></div><div class=\"diagram-step\"><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><span class=\"diagram-pill\">authorize</span></div><div class=\"diagram-card\" data-rough><strong>Dispatch</strong><small class=\"diagram-muted\">identity authority</small><span class=\"diagram-pill accent\">authenticates human</span></div><div class=\"diagram-step\"><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><span class=\"diagram-pill accent\">302 + signed JWT</span></div><div class=\"diagram-card\" data-rough><strong>App callback</strong><small class=\"diagram-muted\">verify signature · scope:identity · exp &le; 2 min</small><span class=\"diagram-pill ok\">JIT-link by email</span><span class=\"diagram-pill ok\">mint local session</span></div></div>",
32
+ "css": ".diagram-sso{display:flex;align-items:stretch;gap:12px;flex-wrap:wrap}.diagram-sso .diagram-card{display:flex;flex-direction:column;gap:6px;padding:14px 16px;min-width:150px}.diagram-sso .diagram-step{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}.diagram-sso .diagram-arrow{font-size:22px;line-height:1}"
33
+ }
34
+ ```
35
+
29
36
  1. **App → Dispatch (authorize).** The app sends the user to the identity authority:
30
37
 
31
38
  ```
@@ -35,6 +42,24 @@ The flow is a standard authorize → signed-token → callback redirect, with em
35
42
  &state=<csrf-state>
36
43
  ```
37
44
 
45
+ ```an-api title="Identity authorize endpoint"
46
+ {
47
+ "method": "GET",
48
+ "path": "/_agent-native/identity/authorize",
49
+ "summary": "Dispatch (identity authority) authenticates the human and redirects back with a signed identity token",
50
+ "auth": "Dispatch session (interactive login if none)",
51
+ "params": [
52
+ { "name": "app", "in": "query", "type": "string", "required": true, "description": "The requesting app identifier." },
53
+ { "name": "redirect_uri", "in": "query", "type": "string", "required": true, "description": "App callback URL. Validated against a strict allowlist (`*.agent-native.com` or localhost by default)." },
54
+ { "name": "state", "in": "query", "type": "string", "required": true, "description": "CSRF state echoed back on the redirect." }
55
+ ],
56
+ "responses": [
57
+ { "status": "302", "description": "Redirects to `redirect_uri` carrying a short-lived `A2A_SECRET`-signed identity JWT (`scope: \"identity\"`, `exp` ≤ 2 minutes) plus the original `state`." },
58
+ { "status": "400", "description": "`redirect_uri` failed allowlist validation (cross-origin, scheme-relative `//host`, or unlisted suffix)." }
59
+ ]
60
+ }
61
+ ```
62
+
38
63
  2. **Dispatch authenticates the human.** If the user already has a Dispatch session, this is transparent. If not, Dispatch shows its own normal login (email/password, Google, etc. — see [Authentication](/docs/authentication)). Dispatch is just a regular agent-native app here; it is not running a special auth mode.
39
64
 
40
65
  3. **Dispatch → App (signed identity token).** Dispatch validates `redirect_uri` against a strict allowlist and 302-redirects back to the app's `redirect_uri` carrying a short-lived **`A2A_SECRET`-signed identity JWT**. The token's claims are intentionally minimal:
@@ -75,6 +100,22 @@ The whole model rests on a few deliberately small guarantees:
75
100
  - **Additive-only identity writes.** Linking either reuses an existing same-email account untouched or inserts a new one. No update, rename, repoint, or delete of identity rows ever happens on this path.
76
101
  - **Off by default.** With `AGENT_NATIVE_IDENTITY_HUB_URL` unset the entire feature is inert.
77
102
 
103
+ ```an-callout
104
+ {
105
+ "tone": "success",
106
+ "body": "**Safe to enable, safe to revert.** Identity writes are **additive only** — an existing same-email account is reused untouched, and a new email just inserts a fresh row. There is no schema change and nothing to migrate, so flipping `AGENT_NATIVE_IDENTITY_HUB_URL` on or off is fully reversible at any time, per app."
107
+ }
108
+ ```
109
+
110
+ The just-in-time link is a single decision keyed entirely on the verified email:
111
+
112
+ ```an-diagram title="JIT-link decision" summary="Linking is keyed on the verified email and is additive only — existing accounts are reused unchanged, new emails create a fresh local user."
113
+ {
114
+ "html": "<div class=\"diagram-jit\"><div class=\"diagram-node\" data-rough>Verified email<br><small class=\"diagram-muted\">from signed identity JWT</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-branch\"><div class=\"diagram-box\" data-rough>Local user exists?<span class=\"diagram-pill ok\">yes &rarr; reuse unchanged</span><span class=\"diagram-pill accent\">no &rarr; create local user</span></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>Mint normal local session</div></div></div>",
115
+ "css": ".diagram-jit{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-jit .diagram-node{display:flex;flex-direction:column;gap:4px;padding:12px 14px}.diagram-jit .diagram-branch{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-jit .diagram-box{display:flex;flex-direction:column;gap:6px;padding:12px 14px}.diagram-jit .diagram-arrow{font-size:22px;line-height:1}"
116
+ }
117
+ ```
118
+
78
119
  ## Self-hosting {#self-hosting}
79
120
 
80
121
  Any Dispatch deployment can serve as the identity hub — you are not limited to `dispatch.agent-native.com`. Set `AGENT_NATIVE_IDENTITY_HUB_URL` on each client app to point at your Dispatch instance:
@@ -7,6 +7,13 @@ description: "Connect a portable SQL database to your agent-native app and write
7
7
 
8
8
  Agent-native apps use [Drizzle ORM](https://orm.drizzle.team) and support portable SQL backends. For anything beyond local development, connect a persistent SQL database — Postgres, libSQL/Turso, or another Drizzle-compatible backend — by setting `DATABASE_URL`. When that variable is unset, the app falls back to a zero-config local SQLite file so you can start developing immediately.
9
9
 
10
+ ```an-diagram title="One schema, many backends" summary="App code uses the framework's dialect-agnostic helpers. The dialect is auto-detected from DATABASE_URL at runtime; unset means a local SQLite file."
11
+ {
12
+ "html": "<div class=\"diagram-db\"><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">@agent-native/core/db/schema</span><small class=\"diagram-muted\">table · text · integer · real · now</small><small class=\"diagram-muted\">+ Drizzle query DSL</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>DATABASE_URL<br><small class=\"diagram-muted\">dialect auto-detected</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-grid\"><span class=\"diagram-pill\">Postgres<br><small class=\"diagram-muted\">Neon · Supabase</small></span><span class=\"diagram-pill\">libSQL / Turso</span><span class=\"diagram-pill\">Cloudflare D1</span><span class=\"diagram-pill warn\">SQLite file<br><small class=\"diagram-muted\">unset = local dev only</small></span></div></div>",
13
+ "css": ".diagram-db{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-db .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 16px}.diagram-db .diagram-arrow{font-size:22px;line-height:1}.diagram-db .diagram-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}"
14
+ }
15
+ ```
16
+
10
17
  ## Local default: SQLite file {#default-sqlite}
11
18
 
12
19
  When `DATABASE_URL` is not set, the app creates a SQLite database at `data/app.db`. This is the zero-config default for local development — no setup required. It is meant for development only; for production, set `DATABASE_URL` to a persistent SQL database.
@@ -80,6 +87,29 @@ export const tasks = table("tasks", {
80
87
  | `real` | Float column — `real` on SQLite, `double precision` on Postgres |
81
88
  | `now` | Dialect-agnostic current timestamp for `.default(now())` |
82
89
 
90
+ The `tasks` table above defines the same columns on every backend:
91
+
92
+ ```an-schema title="The tasks table" summary="Defined once with the framework helpers; the dialect is chosen at runtime from DATABASE_URL."
93
+ {
94
+ "entities": [
95
+ {
96
+ "id": "tasks",
97
+ "name": "tasks",
98
+ "note": "Domain table. Add owner_email (or ...ownableColumns()) so SQL-level scoping can filter rows to the authenticated user.",
99
+ "fields": [
100
+ { "name": "id", "type": "text", "pk": true, "nullable": false },
101
+ { "name": "title", "type": "text", "nullable": false },
102
+ { "name": "priority", "type": "integer", "nullable": false, "note": "default 0" },
103
+ { "name": "weight", "type": "real", "nullable": true },
104
+ { "name": "done", "type": "integer (boolean mode)", "nullable": false, "note": "default false; maps to a Postgres boolean" },
105
+ { "name": "owner_email", "type": "text", "nullable": false, "note": "enables data scoping" },
106
+ { "name": "created_at", "type": "text", "nullable": false, "note": "default now()" }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ ```
112
+
83
113
  Never import from `drizzle-orm/sqlite-core` or `drizzle-orm/pg-core` directly. Always use `@agent-native/core/db/schema`.
84
114
 
85
115
  Tables that store user-facing data must include an `owner_email` column so the framework's SQL-level scoping can filter rows to the authenticated user — see [Security](/docs/security#data-scoping). Tables that also support sharing with other users or orgs should spread `...ownableColumns()` instead, which adds `owner_email`, `org_id`, and `visibility` in one call — see [Sharing](/docs/sharing#building).
@@ -132,27 +162,17 @@ All database schema updates must be **strictly additive**.
132
162
 
133
163
  Instead of pushing directly, schema changes should be applied via SQL migrations executed at application startup. Implement additive migrations within a server plugin (e.g., `server/plugins/db.ts`) by invoking the framework's `runMigrations()` helper:
134
164
 
135
- ```ts
136
- import { runMigrations } from "@agent-native/core/db";
137
-
138
- export default runMigrations(
139
- [
140
- {
141
- version: 1,
142
- sql: `ALTER TABLE projects ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0`,
143
- },
144
- {
145
- // Dialect-gated: runs only on the matching backend. Omit the other key
146
- // to make it a no-op on that dialect.
147
- version: 2,
148
- sql: {
149
- postgres: `ALTER TABLE projects ADD COLUMN IF NOT EXISTS tsv tsvector`,
150
- sqlite: `SELECT 1`, // no-op; tsvector is Postgres-only
151
- },
152
- },
153
- ],
154
- { table: "my_app_migrations" },
155
- );
165
+ ```an-annotated-code title="An additive migration plugin"
166
+ {
167
+ "filename": "server/plugins/db.ts",
168
+ "language": "ts",
169
+ "code": "import { runMigrations } from \"@agent-native/core/db\";\n\nexport default runMigrations(\n [\n {\n version: 1,\n sql: `ALTER TABLE projects ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0`,\n },\n {\n // Dialect-gated: runs only on the matching backend. Omit the other key\n // to make it a no-op on that dialect.\n version: 2,\n sql: {\n postgres: `ALTER TABLE projects ADD COLUMN IF NOT EXISTS tsv tsvector`,\n sqlite: `SELECT 1`, // no-op; tsvector is Postgres-only\n },\n },\n ],\n { table: \"my_app_migrations\" },\n);",
170
+ "annotations": [
171
+ { "lines": "6-7", "label": "Additive only", "note": "`ADD COLUMN IF NOT EXISTS` is safe to re-run and never drops data. Renames look like drop+create to Drizzle, so add-then-migrate instead." },
172
+ { "lines": "13-16", "label": "Dialect gating", "note": "Pass an object keyed by dialect to run different SQL per backend. Make the other key a no-op (`SELECT 1`) for Postgres-only or SQLite-only features." },
173
+ { "lines": "19", "label": "Per-app version table", "note": "Each app tracks its own applied versions so migrations are idempotent across restarts and instances." }
174
+ ]
175
+ }
156
176
  ```
157
177
 
158
178
  ## Environment Variables {#environment-variables}
@@ -28,6 +28,13 @@ npx @agent-native/core@latest deploy
28
28
 
29
29
  Each app is built with `APP_BASE_PATH=/<name>` and `VITE_APP_BASE_PATH=/<name>`, then packaged for the target Nitro preset. Cloudflare Pages is the default preset and uses a generated dispatcher worker at `dist/_worker.js`; Netlify uses one function per app in `.netlify/functions-internal/<app>-server` plus generated redirects; Vercel writes a workspace-level `.vercel/output` using the Build Output API.
30
30
 
31
+ ```an-diagram title="One origin, many apps" summary="Each workspace app is built with its own base path and mounted under a path prefix on a single origin — so login and cross-app A2A are same-origin and free."
32
+ {
33
+ "html": "<div class=\"diagram-ws\"><div class=\"diagram-panel\" data-rough><strong>https://your-agents.com</strong><div class=\"diagram-row\"><span class=\"diagram-pill accent\">/mail/*</span><small class=\"diagram-muted\">apps/mail</small></div><div class=\"diagram-row\"><span class=\"diagram-pill accent\">/calendar/*</span><small class=\"diagram-muted\">apps/calendar</small></div><div class=\"diagram-row\"><span class=\"diagram-pill accent\">/forms/*</span><small class=\"diagram-muted\">apps/forms</small></div></div><div class=\"diagram-col wins\"><span class=\"diagram-pill ok\">shared login session</span><span class=\"diagram-pill ok\">zero-config cross-app A2A</span></div></div>",
34
+ "css": ".diagram-ws{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.diagram-ws .diagram-panel{display:flex;flex-direction:column;gap:6px;padding:14px 16px}.diagram-ws .diagram-row{display:flex;align-items:center;gap:8px}.diagram-ws .wins{display:flex;flex-direction:column;gap:8px;align-items:flex-start}"
35
+ }
36
+ ```
37
+
31
38
  Same-origin deploy gives you two big wins for free:
32
39
 
33
40
  - **Shared login session** — log into any app, every app is logged in.
@@ -63,16 +70,26 @@ Per-app independent deploy is still supported — just `cd apps/<name> && npx @a
63
70
 
64
71
  When you run `npx @agent-native/core@latest build`, Nitro builds both the client SPA and the server API into `.output/`:
65
72
 
66
- ```text
67
- .output/
68
- public/ # Built SPA (static assets)
69
- server/
70
- index.mjs # Server entry point
71
- chunks/ # Server code chunks
73
+ ```an-file-tree title="Build output"
74
+ {
75
+ "entries": [
76
+ { "path": ".output/", "note": "self-contained — copy to any environment and run" },
77
+ { "path": ".output/public/", "note": "built SPA (static assets)" },
78
+ { "path": ".output/server/index.mjs", "note": "server entry point" },
79
+ { "path": ".output/server/chunks/", "note": "server code chunks" }
80
+ ]
81
+ }
72
82
  ```
73
83
 
74
84
  The output is self-contained — copy `.output/` to any environment and run it.
75
85
 
86
+ ```an-diagram title="Build to deploy" summary="One source tree builds to a Nitro preset; the same self-contained output runs on Node, Vercel, Netlify, Cloudflare, AWS, or Deno. Every instance points at the same persistent DATABASE_URL."
87
+ {
88
+ "html": "<div class=\"diagram-deploy\"><div class=\"diagram-box\" data-rough>App source</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">build</span><small class=\"diagram-muted\">Nitro preset</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-grid\"><span class=\"diagram-pill\">Node.js</span><span class=\"diagram-pill\">Vercel</span><span class=\"diagram-pill\">Netlify</span><span class=\"diagram-pill\">Cloudflare</span><span class=\"diagram-pill\">AWS Lambda</span><span class=\"diagram-pill\">Deno</span></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough>Persistent DATABASE_URL<br><small class=\"diagram-muted\">shared by every instance</small></div></div>",
89
+ "css": ".diagram-deploy{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-deploy .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 16px}.diagram-deploy .diagram-arrow{font-size:22px;line-height:1}.diagram-deploy .diagram-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}"
90
+ }
91
+ ```
92
+
76
93
  ## Setting the Preset {#setting-the-preset}
77
94
 
78
95
  By default, Nitro builds for Node.js. To target a different platform, set the preset in your `vite.config.ts`:
@@ -11,6 +11,13 @@ Dispatch is the central app that sits in front of every other app in your worksp
11
11
 
12
12
  Without Dispatch, every app in a multi-app workspace ends up re-implementing the same plumbing: its own Slack bot, its own secret store, its own scheduled jobs, its own copy of the workspace's instructions. Rotating one API key turns into ten redeployments. Adding a new policy turns into ten copy-pastes. Dispatch centralizes all of that in one app so the others stay focused on their domain.
13
13
 
14
+ ```an-diagram title="Dispatch as the workspace control plane" summary="One inbox, one vault, one MCP gateway, and shared resources sit in front of the domain apps, which Dispatch reaches as A2A peers."
15
+ {
16
+ "html": "<div class=\"dsp-hub\"><div class=\"diagram-node\">Users &amp; external agents<br><small class=\"diagram-muted\">Slack · email · Telegram · WhatsApp · MCP</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-panel dsp-control\" data-rough><span class=\"diagram-pill accent\">Dispatch &mdash; control plane</span><div class=\"dsp-caps\"><span class=\"diagram-pill\">Central inbox</span><span class=\"diagram-pill\">Secret vault</span><span class=\"diagram-pill\">Cross-app delegation</span><span class=\"diagram-pill\">MCP gateway</span><span class=\"diagram-pill\">Workspace resources</span></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"dsp-peers\"><div class=\"diagram-box\" data-rough>Mail</div><div class=\"diagram-box\" data-rough>Calendar</div><div class=\"diagram-box\" data-rough>Analytics</div></div><small class=\"diagram-muted\">domain apps &mdash; A2A peers</small></div>",
17
+ "css": ".dsp-hub{display:flex;flex-direction:column;align-items:center;gap:10px}.dsp-hub .dsp-control{display:flex;flex-direction:column;align-items:center;gap:10px;width:100%}.dsp-hub .dsp-caps{display:flex;gap:8px;flex-wrap:wrap;justify-content:center}.dsp-hub .dsp-peers{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}"
18
+ }
19
+ ```
20
+
14
21
  ## When you want Dispatch {#when}
15
22
 
16
23
  Reach for Dispatch when any of these are true:
@@ -55,6 +62,19 @@ Dispatch auto-discovers the other apps in your workspace as A2A peers — no man
55
62
 
56
63
  Dispatch can be the single MCP connector for external agents: add `https://dispatch.agent-native.com/_agent-native/mcp` once in Claude, ChatGPT, Codex, or Cursor, and one authorization reaches every granted workspace app instead of one connector per app. See [External Agents](/docs/external-agents) for the full connect flow, app grants, OAuth, and inline MCP App previews.
57
64
 
65
+ ```an-api
66
+ {
67
+ "method": "POST",
68
+ "path": "/_agent-native/mcp",
69
+ "summary": "Unified MCP gateway endpoint",
70
+ "description": "The single MCP connector URL external agents add (e.g. `https://dispatch.agent-native.com/_agent-native/mcp`). One authorization here reaches every **granted** workspace app instead of wiring one connector per app. App grants, OAuth, and inline MCP App previews are covered in [External Agents](/docs/external-agents).",
71
+ "auth": "Standard remote MCP OAuth, handled by the framework. The granted-app set scopes which workspace apps the connector can reach.",
72
+ "responses": [
73
+ { "status": "200", "description": "MCP JSON-RPC response — tools, resources, and MCP App UI resources aggregated across granted workspace apps." }
74
+ ]
75
+ }
76
+ ```
77
+
58
78
  ### Workspace resources
59
79
 
60
80
  Skills, guardrail instructions, agent profiles, and reference resources can be authored once in Dispatch and inherited by the rest of the workspace. Resources with **All apps** scope are global: Dispatch stores them once at workspace scope, and every app agent reads them at runtime. They are not copied into each app, and there is no manual workspace-resource sync step. App shared resources and personal resources can override or narrow the workspace defaults locally.
@@ -90,6 +110,13 @@ Walk through one example end-to-end. A user DMs the bot: _"summarize last week's
90
110
  4. **Reply posted in thread.** The analytics agent returns a result. Dispatch formats it and posts back into the same Slack thread the user wrote in, using the linked identity if there is one (so the agent acts with the requester's permissions, not the workspace owner's).
91
111
  5. **Recovery if anything dies.** If the processor crashes mid-flight — A2A timeout, downstream agent error, function freeze — a retry job sweeps stuck tasks every 60 seconds and re-fires the processor. Up to three attempts before the task is marked `failed`.
92
112
 
113
+ ```an-diagram title="A Slack message through Dispatch" summary="Slack enqueues into SQL, a fresh execution drains it, the Dispatch agent delegates the domain work over A2A, and the reply lands back in the originating thread. A 60s retry job recovers anything that dies mid-flight."
114
+ {
115
+ "html": "<div class=\"dsp-flow\"><div class=\"dsp-row\"><div class=\"diagram-node\">Slack DM<br><small class=\"diagram-muted\">\"summarize last week's signups\"</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\" data-rough><strong>/slack/webhook</strong><br><small class=\"diagram-muted\">verify + INSERT pending task</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-pill ok\">200</div></div><div class=\"dsp-row\"><div class=\"diagram-box\" data-rough><strong>fresh processor</strong><br><small class=\"diagram-muted\">claim task · start agent loop</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">Dispatch agent decides</span><small class=\"diagram-muted\">analytics intent &rarr; call-agent</small></div></div><div class=\"dsp-row\"><div class=\"diagram-box\" data-rough>Analytics app<br><small class=\"diagram-muted\">A2A peer · runs the SQL work</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-pill ok\">reply posted in thread</div></div><div class=\"diagram-panel dsp-retry\" data-rough><span class=\"diagram-pill warn\">recovery</span> <span class=\"diagram-muted\">if the processor crashes &mdash; A2A timeout, downstream error, freeze &mdash; the 60s retry job re-fires it (&le;3 attempts) so the Slack reply still arrives</span> <span class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&#8635;</span></div></div>",
116
+ "css": ".dsp-flow{display:flex;flex-direction:column;gap:12px}.dsp-flow .dsp-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dsp-flow .center{display:flex;flex-direction:column;align-items:center;gap:4px}.dsp-flow .dsp-retry{display:flex;align-items:center;gap:8px;flex-wrap:wrap}"
117
+ }
118
+ ```
119
+
93
120
  The same flow applies for email, Telegram, and WhatsApp — only the adapter changes.
94
121
 
95
122
  ## Reliability story {#reliability}