@agent-native/core 0.63.2 → 0.63.4
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/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +23 -19
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +10 -11
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
- package/dist/client/blocks/library/wireframe.js +2 -1
- package/dist/client/blocks/library/wireframe.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -1
- package/dist/server/auth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +50 -5
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/styles/blocks.css +25 -3
- package/docs/content/a2a-protocol.md +48 -10
- package/docs/content/actions.md +35 -16
- package/docs/content/agent-mentions.md +25 -32
- package/docs/content/agent-surfaces.md +31 -0
- package/docs/content/agent-teams.md +17 -14
- package/docs/content/agent-web-surfaces.md +24 -15
- package/docs/content/authentication.md +21 -0
- package/docs/content/automations.md +37 -21
- package/docs/content/blueprint-installer.md +7 -0
- package/docs/content/cli-adapters.md +7 -0
- package/docs/content/client.md +14 -0
- package/docs/content/cloneable-saas.md +14 -0
- package/docs/content/code-agents-ui.md +27 -0
- package/docs/content/components.md +21 -0
- package/docs/content/context-awareness.md +33 -48
- package/docs/content/creating-templates.md +43 -52
- package/docs/content/cross-app-sso.md +41 -0
- package/docs/content/database.md +41 -21
- package/docs/content/deployment.md +23 -6
- package/docs/content/dispatch.md +27 -0
- package/docs/content/drop-in-agent.md +26 -27
- package/docs/content/durable-resume.md +13 -1
- package/docs/content/embedding-sdk.md +14 -0
- package/docs/content/evals.md +14 -0
- package/docs/content/extensions.md +19 -0
- package/docs/content/external-agents.md +38 -1
- package/docs/content/faq.md +14 -0
- package/docs/content/file-uploads.md +20 -0
- package/docs/content/frames.md +14 -0
- package/docs/content/getting-started.md +34 -16
- package/docs/content/harness-agents.md +14 -0
- package/docs/content/human-approval.md +15 -30
- package/docs/content/key-concepts.md +37 -0
- package/docs/content/local-file-mode.md +26 -19
- package/docs/content/mcp-apps.md +36 -1
- package/docs/content/mcp-clients.md +49 -0
- package/docs/content/mcp-protocol.md +36 -0
- package/docs/content/messaging.md +34 -8
- package/docs/content/migration-workbench.md +7 -0
- package/docs/content/multi-app-workspace.md +29 -16
- package/docs/content/multi-tenancy.md +14 -0
- package/docs/content/native-chat-ui.md +20 -3
- package/docs/content/notifications.md +30 -0
- package/docs/content/observability.md +20 -1
- package/docs/content/observational-memory.md +14 -0
- package/docs/content/onboarding.md +32 -41
- package/docs/content/plan-plugin.md +14 -0
- package/docs/content/pr-visual-recap.md +14 -0
- package/docs/content/processors.md +7 -0
- package/docs/content/progress.md +23 -0
- package/docs/content/pure-agent-apps.md +7 -0
- package/docs/content/real-time-collaboration.md +19 -18
- package/docs/content/recurring-jobs.md +22 -19
- package/docs/content/routing.md +8 -0
- package/docs/content/sandbox-adapters.md +19 -0
- package/docs/content/security.md +38 -0
- package/docs/content/server.md +47 -25
- package/docs/content/sharing.md +50 -0
- package/docs/content/skills-guide.md +27 -42
- package/docs/content/template-analytics.md +71 -41
- package/docs/content/template-assets.md +76 -3
- package/docs/content/template-brain.md +89 -1
- package/docs/content/template-calendar.md +86 -58
- package/docs/content/template-chat.md +25 -9
- package/docs/content/template-clips.md +124 -16
- package/docs/content/template-content.md +146 -47
- package/docs/content/template-design.md +62 -2
- package/docs/content/template-dispatch.md +56 -9
- package/docs/content/template-forms.md +69 -13
- package/docs/content/template-mail.md +73 -26
- package/docs/content/template-plan.md +80 -1
- package/docs/content/template-slides.md +95 -74
- package/docs/content/template-videos.md +73 -52
- package/docs/content/tracking.md +21 -0
- package/docs/content/using-your-agent.md +14 -0
- package/docs/content/voice-input.md +31 -10
- package/docs/content/what-is-agent-native.md +25 -13
- package/docs/content/workspace-connections.md +49 -0
- package/docs/content/workspace-management.md +24 -0
- package/docs/content/workspace.md +50 -19
- package/docs/content/writing-agent-instructions.md +7 -0
- package/package.json +1 -1
package/docs/content/client.md
CHANGED
|
@@ -15,6 +15,13 @@ For file-based routing — adding pages, dynamic params, and navigation — see
|
|
|
15
15
|
|
|
16
16
|
The primary way to read and write app data from the browser is through the action hooks. Never hand-write `fetch` calls to `/_agent-native/*` routes — use the named helpers instead (see [Actions](/docs/actions)).
|
|
17
17
|
|
|
18
|
+
```an-diagram title="The browser data loop" summary="Hooks read and write through actions; useDbSync watches the database so agent and background writes refetch the same caches automatically."
|
|
19
|
+
{
|
|
20
|
+
"html": "<div class=\"diagram-client\"><div class=\"diagram-col\"><div class=\"diagram-node\">useActionQuery<br><small class=\"diagram-muted\">cached read</small></div><div class=\"diagram-node\">useActionMutation<br><small class=\"diagram-muted\">write + invalidate</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">↔</div><div class=\"diagram-box\" data-rough>Actions<br><small class=\"diagram-muted\">/_agent-native/actions/*</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">↔</div><div class=\"diagram-panel\" data-rough><strong>SQL database</strong></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">↻</div><div class=\"diagram-pill ok\">useDbSync → refetch on change</div></div>",
|
|
21
|
+
"css": ".diagram-client{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-client .diagram-col{display:flex;flex-direction:column;gap:10px}.diagram-client .diagram-arrow{font-size:22px;line-height:1}"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
18
25
|
```tsx
|
|
19
26
|
import {
|
|
20
27
|
useActionQuery,
|
|
@@ -354,6 +361,13 @@ function DashboardView({ id }) {
|
|
|
354
361
|
- **UI-Initiated mutations:** When you execute an action from the UI using `useActionMutation`, the mutation immediately fires a local event with `source: "action"` on success. This triggers an **instant, optimistic refetch** of all query keys depending on that action, avoiding visual delay.
|
|
355
362
|
- **Background or Agent Mutations:** When the AI agent, a webhook, or a background worker mutates data, the update is broadcast to the client. The client's `useDbSync` captures this either instantly over SSE (Server-Sent Events) or falls back to the **2-second polling tick**. The query key version then bumps, triggering a background refetch.
|
|
356
363
|
|
|
364
|
+
```an-diagram title="Two paths to a refetch" summary="A local mutation invalidates its own caches instantly; a remote write reaches this tab over SSE, or the polling tick as a fallback."
|
|
365
|
+
{
|
|
366
|
+
"html": "<div class=\"diagram-latency\"><div class=\"diagram-col\"><div class=\"diagram-card\" data-rough><span class=\"diagram-pill ok\">This tab</span><strong>useActionMutation</strong><small class=\"diagram-muted\">fires source: \"action\" on success → instant local refetch</small></div><div class=\"diagram-card\" data-rough><span class=\"diagram-pill accent\">Agent · webhook · other tab</span><strong>Remote write</strong><small class=\"diagram-muted\">SSE push, or the ~2s polling tick as fallback → version bumps → background refetch</small></div></div></div>",
|
|
367
|
+
"css": ".diagram-latency .diagram-col{display:flex;flex-direction:column;gap:12px}.diagram-latency .diagram-card{display:flex;flex-direction:column;gap:4px;padding:14px 16px}"
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
357
371
|
## cn(...inputs) {#cn}
|
|
358
372
|
|
|
359
373
|
Utility for merging class names (clsx + tailwind-merge):
|
|
@@ -51,6 +51,13 @@ This isn't a theoretical claim. The framework's author runs his actual inbox on
|
|
|
51
51
|
|
|
52
52
|
The path from "I want my own SaaS" to "I have my own SaaS" is short:
|
|
53
53
|
|
|
54
|
+
```an-diagram title="Fork and customize" summary="Pick a finished product, brand it, evolve it in plain English, and ship it to your own domain."
|
|
55
|
+
{
|
|
56
|
+
"html": "<div class=\"diagram-fork\"><div class=\"diagram-card\"><span class=\"diagram-pill\">1</span><strong>Pick</strong><small class=\"diagram-muted\">a complete template</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</div><div class=\"diagram-card\"><span class=\"diagram-pill\">2</span><strong>Brand</strong><small class=\"diagram-muted\">name, colors, logo, copy</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</div><div class=\"diagram-card\"><span class=\"diagram-pill accent\">3</span><strong>Customize</strong><small class=\"diagram-muted\">ask the agent ↻</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</div><div class=\"diagram-card\"><span class=\"diagram-pill ok\">4</span><strong>Ship</strong><small class=\"diagram-muted\">your own domain</small></div></div>",
|
|
57
|
+
"css": ".diagram-fork{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.diagram-fork .diagram-card{display:flex;flex-direction:column;gap:6px;padding:14px 16px;min-width:130px}.diagram-fork .diagram-arrow{font-size:22px;line-height:1}"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
54
61
|
1. **Pick a template.** Use the CLI picker, or browse the docs and pick one to start from.
|
|
55
62
|
2. **Brand it.** Change the name, colors, logo, and copy. Most templates expose this in a single config file.
|
|
56
63
|
3. **Customize it.** Ask the agent to add the column you need, change how the inbox groups, connect to your internal API, add a new view. The agent edits the code; you review the diff.
|
|
@@ -67,6 +74,13 @@ A traditional fork-the-codebase model breaks down at scale: every user maintaini
|
|
|
67
74
|
|
|
68
75
|
The result: Claude-Code-level flexibility for each user, with normal SaaS deployment economics.
|
|
69
76
|
|
|
77
|
+
```an-diagram title="Why per-user forks scale" summary="Two ideas keep the fork-and-customize model practical: the agent does the maintenance, and per-user customization lives in SQL — not in per-user code."
|
|
78
|
+
{
|
|
79
|
+
"html": "<div class=\"diagram-why\"><div class=\"diagram-panel\" data-rough><strong>Shared codebase</strong><small class=\"diagram-muted\">one app, deployed once</small><div class=\"diagram-pill accent\">agent does the maintenance</div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">↔</div><div class=\"diagram-panel\" data-rough><strong>Per-user layer in SQL</strong><small class=\"diagram-muted\">skills · memory · instructions · MCP · sub-agents</small><div class=\"diagram-pill ok\">no per-user code</div></div></div>",
|
|
80
|
+
"css": ".diagram-why{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.diagram-why .diagram-panel{display:flex;flex-direction:column;gap:8px;padding:14px 18px;min-width:240px;flex:1}.diagram-why .diagram-arrow{font-size:24px;line-height:1}"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
70
84
|
## Don't want to fork? {#hosted}
|
|
71
85
|
|
|
72
86
|
You don't have to. Every template is also available as a hosted app on `agent-native.com` — `mail.agent-native.com`, `calendar.agent-native.com`, and so on. Use the hosted version for free or paid; fork only when you want to change something the hosted version doesn't expose.
|
|
@@ -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\">↓</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\">↓</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.
|
|
@@ -366,8 +373,28 @@ The connection is outbound-only from Desktop:
|
|
|
366
373
|
5. Mobile reads `hosts`, `runs`, and `transcript` from Dispatch; it never talks
|
|
367
374
|
directly to the desktop.
|
|
368
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\">→</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\">←</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
|
+
|
|
369
383
|
The canonical remote relay endpoints are:
|
|
370
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
|
+
|
|
371
398
|
| Method | Route | Caller | Purpose |
|
|
372
399
|
| ---------- | -------------------------------------------------------- | --------------- | ------------------------------------------- |
|
|
373
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\"><AgentSidebar></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\"><AgentPanel> · <AgentChatSurface></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\"><AssistantChat> + 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\"><PromptComposer> · <AgentConversation></span><small class=\"diagram-muted\">Composer and transcript primitives only.</small></div><div class=\"diagram-rail\" data-rough>Same runtime: actions · thread state · 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 · 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\">→</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\">→</div><div class=\"diagram-box\"><PresenceBar> · <LiveCursorOverlay><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\">→</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">view-screen</span><small class=\"diagram-muted\">reads state · fetches records</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</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\">↻</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
|
-
```
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
import { eq, inArray } from "drizzle-orm";
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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\">→</div><div class=\"diagram-box\" data-rough>Server stores source<br>on the event</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</div><div class=\"diagram-card col\"><div class=\"diagram-pill warn\">source == TAB_ID → ignored</div><small class=\"diagram-muted\">no refetch, no flicker</small><div class=\"diagram-pill ok\">agent / other tab → 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
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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\">→</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\">→</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\">↻</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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
import { nanoid } from "nanoid";
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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\">→</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\">→</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 ≤ 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\">→</div><div class=\"diagram-branch\"><div class=\"diagram-box\" data-rough>Local user exists?<span class=\"diagram-pill ok\">yes → reuse unchanged</span><span class=\"diagram-pill accent\">no → create local user</span></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">→</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:
|
package/docs/content/database.md
CHANGED
|
@@ -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\">→</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\">→</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
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
[
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
```
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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\">→</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\">→</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\">→</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`:
|